简单易懂的修复liftM2方法:
原始示例中的问题在于ap
函数与liftM
函数有些不同。 ap
函数接受一个被封装在单子中的函数,并将其应用于一个被封装在单子中的参数。但是,liftMn
函数接受一个“正常”的函数(即未被封装在单子中的函数),并将其应用于被封装在单子中的参数。
我将在下面更详细地解释这意味着什么,但要点是,如果您想使用liftM2
函数,则必须将(/)
拿出来,使其成为一个独立的参数放在开头。(因此,在这种情况下,(/)
是“普通”函数。)
let average = liftM2 ((/) . realToFrac . sum) genericLength
let average = liftM2 (/) (realToFrac . sum) genericLength
如原问题中所发帖的,调用
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
都涉及到单子。在解释此处之前,您可以将单子视为一个值可以“封装在其中”的东西。下面我会给出更好的定义。
普通函数应用将普通函数应用于普通输入。liftM
将普通函数应用于封装在单子中的输入,ap
将封装在单子中的函数应用于封装在单子中的输入。
(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
函数中,单子是函数,但是“函数作为单子”可能很难谈论,因此我将从更简单的例子开始。
那么什么是单子?
单子的更好描述是“包含值或生成值或可以从中提取值的东西,但还具有更复杂的功能”。
那是非常模糊的描述,但必须这样,因为“更复杂的事情”可能是许多不同的事情。
单子可能很困惑,但它们的重点在于当您使用单子操作(如
ap
和
liftM
)时,它们将为您处理“更复杂的事情”,因此您只需专注于值。
这可能仍然不是很清楚,因此让我们进行一些例子:
Maybe 单子
ap (Just (10/)) (Just 4)
最简单的单子之一是 'Maybe'。值就是包含在 Just
中的任何内容。因此,如果我们调用 ap
并给它 (Just ourFunction)
和 (Just ourInput)
,那么我们会得到 (Just ourResult)
。
"更加复杂的" 是可能根本没有值存在,您必须允许 Nothing
的情况。
正如前面提到的,使用像 ap
这样的函数的意义在于它为我们处理这些额外的复杂性。对于 Maybe
单子,ap
通过在 Maybe 函数或 Maybe 输入为 Nothing
时返回 Nothing
来处理这个问题。
ap (Just (10/)) Nothing
ap Nothing (Just 4)
列表单子
ap [(10/)] [4]
对于列表单子,其值始终是列表内的内容。因此,ap [我们的函数] [我们的输入]
返回[我们的结果]
。
“更复杂的事情”在于列表中可能有多个内容(或者完全没有内容、只有一个内容)。
对于列表来说,这意味着ap
接受零个或多个函数以及零个或多个输入的列表。它通过返回一个结果列表来处理:每种可能的函数和输入组合都有一个结果。
ap [(10/), (100/)] [5,4,2]
函数作为单子
像genericLength
这样的函数被认为是单子,因为它有一个值(函数的输出),还有一个“更加复杂”的东西(你必须提供输入才能获得值)。
这就是让人有些困惑的地方,因为我们处理多个函数、多个输入和多个结果。虽然所有内容都很明确,但我们必须小心使用术语。
让我们从列表[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 " )
average =
(functionThatTakes [1,2,3,4] and returns "2.5" )
与前面更简单的例子相比,您会发现这个示例仍然具有我们的函数
(10/)
,输入
4
和结果
2.5
。每个元素再次被包装在“更加复杂的东西”中。在这种情况下,“更加复杂的东西”是“
接受[1,2,3,4]并返回...
”的函数。
当然,由于它们是函数,它们不必将
[1,2,3,4]
作为它们的输入。如果它们使用了不同的整数列表(例如
[1,2,3,4,5]
),那么我们将获得不同的结果(例如新函数:
(15/)
,新输入
5
和新值
3
)。
其他例子:
minPlusMax = ap ((+) . minimum) maximum
upperAndLower = ap ((,) . toUpper) toLower
这些也都可以使用liftM2
来定义。
average = liftM2 (/) sum genericLength
minPlusMax = liftM2 (+) minimum maximum
upperAndLower = liftM2 (,) toUpper toLower
let average = liftM2 ((/) . realToFrac) sum genericLength
的代码有效。 - Ørjan Johansenap
定义为(. ((. (return .)) . (>>=))) . (>>=)
。 :-) - awllower