简单的一点:修复 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 应该涉及三个agruments: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 视为可以“包装”值的东西。我会在下面给出一个更好的定义。
普通函数应用程序将普通函数应用于普通输入。 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 是函数,但是“functions-as-monads”可能很难说,所以我将从更简单的例子开始。
那么什么是单子?
对 monad 更好的描述是“包含一个值,或者产生一个值,或者你可以以某种方式从中提取一个值的东西,但它也有更复杂的事情发生。”
这是一个真的模糊的描述,但它必须是,因为“更复杂的东西”可以是很多不同的东西。
Monad 可能会令人困惑,但它们的意义在于,当您使用 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,ap 通过返回 Nothing 来处理这个问题,如果 Maybe 函数或 Maybe 输入是 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] 返回[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 需要做两件事。第一个是读取原始输入并生成新函数的函数。第二个是读取原始输入并产生新输入的函数。最终结果是一个函数,它接受原始输入,并返回与将新函数应用于新输入时相同的结果。
您可能需要多读几遍才能理解它。或者,这里是伪代码:
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