了解 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 . sum
和 genericLength
[=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
.
- 如果
M
是x
,则将λx.x
替换为id
(id
在组合逻辑中通常表示为I
) .
- 如果
M
不包含x
,则将其替换为const M
(const
在组合逻辑中通常表示为K
) .
如果M
是PQ
,也就是词条是λ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应用部分就够了),其中r
是x
的类型。如果我们修改上面的过程:
- 唯一与 reader monad 相关的情况是
M
是 x
。然后我们从"lift"x
到id
,去掉这个变量。下面的其他情况只是将表达式提升为应用函子的机械应用:
- 另一种情况
λx.M
M
不包含 x
,它只是将 M
提升到 reader 应用程序,即 pure M
。实际上,对于 (->) r
,pure
等价于 const
.
- 在最后一种情况下,
<*> :: f (a -> b) -> f a -> f b
是提升到 monad/applicative 的函数应用程序。这正是我们所做的:我们将 P
和 Q
两个部分提升到 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
原始示例中的问题是 ap
与 liftM
函数的工作方式略有不同。 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)
,x2
是 genericLength
。
问题中发布的版本(不起作用的版本)试图仅使用两个参数调用 liftM2
。
解释
我将分几个阶段进行构建。我将从一些特定的值开始,然后构建一个可以采用任何值集的函数。跳转到 TL:DR
的最后一节
在这个例子中,假设数字列表是[1,2,3,4]。这些数字的总和为10,列表的长度为4。平均值为10/4
或2.5
。
为了将其改造成 ap
的正确形式,我们将把它分解为一个函数、一个输入和一个结果。
ourFunction = (10/) -- "divide 10 by"
ourInput = 4
ourResult = 2.5
三种功能应用
ap
和 listM
都涉及 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 操作(如 ap
和 liftM
)时,它们会为您处理“更复杂的事情”,这样您就可以专注于价值观。
这可能还不是很清楚,所以让我们做一些例子:
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
我能够理解 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 . sum
和 genericLength
[=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
.
- 如果
M
是x
,则将λx.x
替换为id
(id
在组合逻辑中通常表示为I
) . - 如果
M
不包含x
,则将其替换为const M
(const
在组合逻辑中通常表示为K
) . 如果
M
是PQ
,也就是词条是λ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应用部分就够了),其中r
是x
的类型。如果我们修改上面的过程:
- 唯一与 reader monad 相关的情况是
M
是x
。然后我们从"lift"x
到id
,去掉这个变量。下面的其他情况只是将表达式提升为应用函子的机械应用: - 另一种情况
λx.M
M
不包含x
,它只是将M
提升到 reader 应用程序,即pure M
。实际上,对于(->) r
,pure
等价于const
. - 在最后一种情况下,
<*> :: f (a -> b) -> f a -> f b
是提升到 monad/applicative 的函数应用程序。这正是我们所做的:我们将P
和Q
两个部分提升到 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
原始示例中的问题是 ap
与 liftM
函数的工作方式略有不同。 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)
,x2
是 genericLength
。
问题中发布的版本(不起作用的版本)试图仅使用两个参数调用 liftM2
。
解释
我将分几个阶段进行构建。我将从一些特定的值开始,然后构建一个可以采用任何值集的函数。跳转到 TL:DR
的最后一节在这个例子中,假设数字列表是[1,2,3,4]。这些数字的总和为10,列表的长度为4。平均值为10/4
或2.5
。
为了将其改造成 ap
的正确形式,我们将把它分解为一个函数、一个输入和一个结果。
ourFunction = (10/) -- "divide 10 by"
ourInput = 4
ourResult = 2.5
三种功能应用
ap
和 listM
都涉及 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 操作(如 ap
和 liftM
)时,它们会为您处理“更复杂的事情”,这样您就可以专注于价值观。
这可能还不是很清楚,所以让我们做一些例子:
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