了解 Haskell 中无点函数中的 `ap`

Understanding `ap` in a point-free function in Haskell

我能够理解 Haskell 中无点函数的基础知识:

addOne x = 1 + x

等式两边都是x,我们简化一下:

addOne = (+ 1)

事实证明,在不同部分两次使用相同参数的函数可以写成无点!

让我举一个基本的例子 average 函数写成:

average xs = realToFrac (sum xs) / genericLength xs

似乎无法简化 xs,但 http://pointfree.io/ 得出:

average = ap ((/) . realToFrac . sum) genericLength

有效。

据我了解,average 与在两个函数上调用 ap 相同,(/) . realToFrac . sumgenericLength[=28= 的组合]

不幸的是,ap 函数对我来说毫无意义,文档 http://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Monad.html#v:ap 声明:

ap :: Monad m => m (a -> b) -> m a -> m b

In many situations, the liftM operations can be replaced by uses of ap,
which promotes function application.

      return f `ap` x1 `ap` ... `ap` xn

is equivalent to

      liftMn f x1 x2 ... xn

但是写作:

let average = liftM2 ((/) . realToFrac . sum) genericLength

不起作用,(给出了很长的类型错误消息,请询问,我会包含它),所以我不明白文档试图说什么。


表达式 ap ((/) . realToFrac . sum) genericLength 是如何工作的?你能用比文档更简单的术语解释 ap 吗?

当 monad m(->) a 时,如您的情况,您可以定义 ap 如下:

ap f g = \x -> f x (g x)

我们可以在您的 pointfree 示例中看到这确实 "works"。

average = ap ((/) . realToFrac . sum) genericLength
average = \x -> ((/) . realToFrac . sum) x (genericLength x)
average = \x -> (/) (realToFrac (sum x)) (genericLength x)
average = \x -> realToFrac (sum x) / genericLength x

我们还可以从一般规律推导出ap

ap f g = do ff <- f ; gg <- g ; return (ff gg)

也就是说,对 do-notation

进行脱糖处理
ap f g = f >>= \ff -> g >>= \gg -> return (ff gg)

如果我们替换 monad 方法的定义

m >>= f = \x -> f (m x) x
return x = \_ -> x

我们得到了之前 ap 的定义(对于我们特定的 monad (->) a)。确实:

app f g 
= f >>= \ff -> g >>= \gg -> return (ff gg)
= f >>= \ff -> g >>= \gg -> \_ -> ff gg
= f >>= \ff -> g >>= \gg _ -> ff gg
= f >>= \ff -> \x -> (\gg _ -> ff gg) (g x) x
= f >>= \ff -> \x -> (\_ -> ff (g x)) x
= f >>= \ff -> \x -> ff (g x)
= f >>= \ff x -> ff (g x)
= \y -> (\ff x -> ff (g x)) (f y) y
= \y -> (\x -> f y (g x)) y
= \y -> f y (g y)

任何 lambda 项都可以重写为仅使用一组合适的 combinators and no lambda abstractions. This process is called abstraciton elimination 的等效项。在此过程中,您希望从内到外删除 lambda 抽象。所以在第一步中你有 λx.M 其中 M 已经没有 lambda 抽象,你想摆脱 x.

  • 如果Mx,则将λx.x替换为idid在组合逻辑中通常表示为I) .
  • 如果M不包含x,则将其替换为const Mconst在组合逻辑中通常表示为K) .
  • 如果MPQ,也就是词条是λx.PQ,你要"push"x里面两部分函数应用程序,以便您可以递归处理这两个部分。这是通过使用定义为 λfgx.(fx)(gx)S 组合子来实现的,也就是说,它接受两个函数并将 x 传递给它们,并将结果一起应用。您可以轻松地验证 λx.PQ 等同于 S(λx.P)(λx.Q),并且我们可以递归地处理两个子项。

    如其他答案中所述,S 组合器在 Haskell 中可用,作为 ap(或 <*>)专用于 reader monad .

reader monad 的出现并非偶然:当解决用等效函数替换 λx.M 的任务时,基本上是将 M :: a 提升到 reader monad r -> a(其实reader应用部分就够了),其中rx的类型。如果我们修改上面的过程:

  • 唯一与 reader monad 相关的情况是 Mx。然后我们从"lift"xid,去掉这个变量。下面的其他情况只是将表达式提升为应用函子的机械应用:
  • 另一种情况 λx.M M 不包含 x,它只是将 M 提升到 reader 应用程序,即 pure M。实际上,对于 (->) rpure 等价于 const.
  • 在最后一种情况下,<*> :: f (a -> b) -> f a -> f b 是提升到 monad/applicative 的函数应用程序。这正是我们所做的:我们将 PQ 两个部分提升到 reader 应用程序,然后使用 <*> 将它们绑定在一起。

可以通过添加更多组合子来进一步改进该过程,从而使生成的项更短。大多数情况下,combinators B and C are used,在 Haskell 中对应函数 (.)flip。同样,对于 reader 应用程序,(.) 只是 fmap/<$>。 (我不知道有这样一个用于表达 flip 的内置函数,但它会被视为 reader 应用程序的 f (a -> b) -> a -> f b 的特化。)

前段时间我写了一篇关于这个的短文:The Monad Reader Issue 17,Reader Monad 和抽象消除。

简单点:修复 liftM2

原始示例中的问题是 apliftM 函数的工作方式略有不同。 ap 接受一个封装在 monad 中的函数,并将其应用于封装在 monad 中的参数。但是 liftMn 函数接受一个“普通”函数(一个 不是 包裹在 monad 中的函数)并将其应用于 是的参数 包裹在单子中。

我将在下面详细解释这意味着什么,但结果是如果你想使用 liftM2,那么你必须将 (/) 拉出来并将其作为一个单独的参数开始。 (所以在这种情况下 (/) 是“正常”函数。)

let average = liftM2 ((/) . realToFrac . sum) genericLength -- does not work
let average = liftM2 (/)   (realToFrac . sum) genericLength -- works

如原问题中所述,调用 liftM2 应该涉及三个参数:liftM2 f x1 x2。这里 f(/)x1(realToFrac . sum)x2genericLength

问题中发布的版本(不起作用的版本)试图仅使用两个参数调用 liftM2

解释

我将分几个阶段进行构建。我将从一些特定的值开始,然后构建一个可以采用任何值集的函数。跳转到 TL:DR

的最后一节

在这个例子中,假设数字列表是[1,2,3,4]。这些数字的总和为10,列表的长度为4。平均值为10/42.5

为了将其改造成 ap 的正确形式,我们将把它分解为一个函数、一个输入和一个结果。

ourFunction =  (10/)    -- "divide 10 by"
ourInput    =    4
ourResult   =    2.5 

三种功能应用

aplistM 都涉及 monad。在解释的这一点上,您可以将 monad 视为可以将值“包裹在其中”的东西。我会在下面给出一个更好的定义。

普通函数应用程序将普通函数应用于普通输入。 liftM 将普通函数应用于包装在 monad 中的输入,ap 将包装在 monad 中的函数应用于包装在 monad 中的输入。

        (10/) 4          -- returns 2.5
liftM   (10/) monad(4)   -- returns monad(2.5)
ap monad(10/) monad(4)   -- returns monad(2.5)

(注意这是伪代码。monad(4)不是实际有效Haskell)。

(注意 liftM 与之前使用的 liftM2 是不同的函数。liftM 接受一个函数并且只有一个参数,这更适合该模式我在描述。)

在上面定义的 average 函数中,monad 是函数,但是“作为 monad 的函数”很难说,所以我将从更简单的示例开始。

那么什么是 monad?

对 monad 的更好描述是“包含一个值,或产生一个值,或者您可以以某种方式从中提取值,但也有更复杂的事情发生的东西。”

那是一个 确实 模糊的描述,但它必须是,因为“更复杂的东西”可以是很多不同的东西。

Monads 可能会令人困惑,但它们的重点是当您使用 monad 操作(如 apliftM)时,它们会为您处理“更复杂的事情”,这样您就可以专注于价值观。

这可能还不是很清楚,所以让我们做一些例子:

Maybe 单子

ap (Just (10/)) (Just 4)   -- result is (Just 2.5)

最简单的单子之一是'Maybe'。该值是 Just 中包含的任何值。所以如果我们调用 ap 并给它 (Just ourFunction)(Just ourInput) 然后我们返回 (Just ourResult).

“更复杂的事情”是那里可能根本没有值,您必须考虑 Nothing 情况。

如前所述,使用像 ap 这样的函数的意义在于它为我们处理了这些额外的复杂问题。对于 Maybe monad,如果 Maybe 函数或 Maybe 输入是 Nothing.

ap 通过返回 Nothing 来处理这个问题
ap (Just (10/)) Nothing    -- result is Nothing
ap Nothing (Just 4)        -- result is Nothing

单子列表

ap [(10/)] [4]    -- result is [2.5]

对于列表 Monad,值是列表中的任何值。所以ap [ourfunction] [ourInput]returns[ourResult].

“更复杂的东西”是列表中可能有不止一件东西(或者只有一件东西,或者什么都没有)。

对于列表,这意味着 ap 接受一个包含零个或多个函数的列表,以及一个包含零个或多个输入的列表。它通过返回一个包含零个或多个结果的列表来处理这个问题:每个可能的函数和输入组合都有一个结果。

ap [(10/), (100/)] [5,4,2]  -- result is [2.0, 2.5, 5.0, 20.0, 25.0, 50.0]

函数作为 Monad

genericLength 这样的函数被认为是 Monad,因为它有一个值(函数的输出),并且它有一个“更复杂的东西”(你必须先提供输入才能获取值)。

这是有点令人困惑的地方,因为我们正在处理多个函数、多个输入和多个结果。一切都很好定义,只是很难描述,所以我们必须小心我们的术语。

让我们从列表 [1,2,3,4] 开始,并将其称为我们的“原始输入”。这就是我们试图找到平均值的列表。这是原始 average 函数中的 xs 参数。

如果我们将原始输入 ([1,2,3,4]) 提供给 genericLength,那么我们会得到值“4”。

我们的另一个功能是((/) . realToFrac . sum)。它获取我们的列表 [1,2,3,4] 并找到总和 (10),将其转换为分数值,然后将其作为 (/) 的第一个参数提供。结果是一个不完整的除法函数正在等待另一个参数。即它以 [1,2,3,4] 作为输入,并产生 (10/) 作为输出。

这一切都符合 ap 为函数定义的方式。对于函数,ap 需要做两件事。第一个是读取原始输入并生成新函数的函数。第二个是读取原始输入并生成新输入的函数。最终结果是一个采用原始输入的函数,returns 与将新函数应用于新输入时得到的结果相同。

您可能需要多读几遍才能理解它。或者,这里是伪代码:

average = 
  ap
    (functionThatTakes [1,2,3,4] and returns "(10/)" )
    (functionThatTakes [1,2,3,4] and returns "  4  " )

-- which means:

average = 
    (functionThatTakes [1,2,3,4] and returns "2.5"  )

如果将其与上面更简单的示例进行比较,您会发现它仍然具有我们的函数 (10/)、我们的输入 4 和我们的结果 2.5。他们每个人都再次被“更复杂的东西”所包裹。在这种情况下,“更复杂的东西”是“function that takes [1,2,3,4] and returns...”。

当然,因为它们是函数,它们没有[1,2,3,4]作为它们的输入。如果他们采用不同的整数列表(例如 [1,2,3,4,5]),那么我们将得到不同的结果(例如新函数:(15/)、新输入 5 和新值 3)。

其他例子

minPlusMax = ap ((+) . minimum) maximum
-- a function that adds the minimum element of a list, to the maximum element


upperAndLower = ap ((,) . toUpper) toLower
-- a function that takes a Char and returns a tuple, with the upper case and lower case versions of a character

这些也可以使用 liftM2.

来定义
average       = liftM2 (/) sum genericLength
minPlusMax    = liftM2 (+) minimum maximum
upperAndLower = liftM2 (,) toUpper toLower