感谢Scott Sauyet和Bergi的答案,我终于理解了。在这个过程中,我感觉还需要一些努力才能把所有的部分拼凑在一起。我将记录一些在这个过程中遇到的问题,希望对某些人有所帮助。
以下是我们试图理解的R.lift的示例:
var madd3 = R.lift((a, b, c) => a + b + c);
madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7]
对我而言,在理解它之前,有三个问题需要回答。
- Fantasy-land的
Apply
规范(我将其称为Apply
)以及Apply#ap
的作用
- Ramda的
R.ap
实现以及Array
与Apply
规范有什么关系
- 柯里化在
R.lift
中扮演了什么角色
理解Apply
规范
在幻想世界中,当一个对象定义了
ap
方法时,它实现了
Apply
规范(该对象还必须通过定义
map
方法来实现
Functor
规范)。
ap
方法具有以下签名:
ap :: Apply f => f a ~> f (a -> b) -> f b
在
fantasy-land的类型签名表示法 中:
=>
声明类型约束,因此上面标记中的 f
指的是类型 Apply
~>
声明方法声明,所以 ap
应该是在 Apply
上声明的函数,它围绕我们称之为 a
的值进行封装(我们将在下面的示例中看到,一些fantasy-land的实现的ap
与此签名不一致,但思想是相同的)
假设我们有两个对象 v
和 u
(v = f a; u = f (a -> b)
),因此这个表达式是有效的 v.ap(u)
,这里需要注意一些事情:
v
和u
都实现了Apply
。 v
保存一个值,u
保存一个函数,但它们具有相同的Apply
“接口”(这将有助于理解下面的下一节,当涉及到R.ap
和Array
时)
- 值
a
和函数a -> b
对Apply
一无所知,函数只是转换值a
。正是Apply
将值和函数放在容器中,而ap
则将它们提取出来,在值上调用函数,然后再放回去。
理解Ramda
的R.ap
R.ap
的签名有两种情况:
应用 f => f (a → b) → f a → f b
: 这与上一节中 Apply#ap
的签名非常相似,区别在于如何调用 ap
(Apply#ap
vs. R.ap
)和参数的顺序。
[a → b] → [a] → [b]
: 如果我们将 Apply f
替换为 Array
,请记住,在上一节中,值和函数必须包装在同一个容器中。这就是为什么当使用 R.ap
与 Array
时,第一个参数是函数列表,即使你只想应用一个函数,也要将它放入一个数组中。
让我们来看一个例子,我正在使用ramda-fantasy
中的Maybe
,它实现了Apply
。这里存在一个不一致之处,即Maybe#ap
的签名是:ap :: Apply f => f (a -> b) ~> f a -> f b
。似乎一些其他的fantasy-land
实现也遵循这种方式,但这不应影响我们的理解:
const R = require('ramda');
const Maybe = require('ramda-fantasy').Maybe;
const a = Maybe.of(2);
const plus3 = Maybe.of(x => x + 3);
const b = plus3.ap(a);
const b2 = R.ap(plus3, a);
console.log(b);
console.log(b2);
理解R.lift
的例子
在R.lift
使用数组的例子中,一个具有3个参数的函数被传递给了R.lift
: var madd3 = R.lift((a, b, c) => a + b + c);
,它如何与三个数组[1, 2, 3], [1, 2, 3], [1]
一起工作呢?请注意,它不是柯里化的。
实际上,在R.liftN
的源代码中(R.lift
委托给它),传入的函数是自动柯里化的,然后它遍历值(在我们的例子中,是三个数组),将其缩减为结果:在每次迭代中,它使用柯里化的函数和一个值(在我们的例子中,是一个数组)调用ap
。很难用语言解释清楚,让我们看看等价的代码:
const R = require('ramda');
const madd3 = (x, y, z) => x + y + z;
const result = R.lift(madd3)([1, 2, 3], [1, 2, 3], [1]);
const result2 = R.ap(R.ap(R.ap([R.curry(madd3)], [1, 2, 3]), [1, 2, 3]), [1]);
console.log(result);
console.log(result2);
一旦理解了计算
result2
的表达式,这个例子就会变得清晰明了。
以下是另一个例子,使用
R.lift
在
Apply
上:
const R = require('ramda');
const Maybe = require('ramda-fantasy').Maybe;
const madd3 = (x, y, z) => x + y + z;
const madd3Curried = Maybe.of(R.curry(madd3));
const a = Maybe.of(1);
const b = Maybe.of(2);
const c = Maybe.of(3);
const sumResult = madd3Curried.ap(a).ap(b).ap(c);
const sumResult2 = R.ap(R.ap(R.ap(madd3Curried, a), b), c);
const sumResult3 = R.lift(madd3)(a, b, c);
console.log(sumResult);
console.log(sumResult2);
console.log(sumResult3);
在评论中,Scott Sauyet提出了一个更好的例子(他提供了很多见解,建议你阅读),这个例子更容易理解,至少它指向了一个方向,即R.lift
计算Array
的笛卡尔积。
var madd3 = R.lift((a, b, c) => a + b + c);
madd3([100, 200], [30, 40, 50], [6, 7]); //=> [136, 137, 146, 147, 156, 157, 236, 237, 246, 247, 256, 257]
希望这能帮到你。