多亏了 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's type signature notation:
-
=> 声明了类型约束,所以上面签名中的f 指的是类型Apply
-
~> 声明了 method 声明,因此 ap 应该是在 Apply 上声明的函数,它包含我们称为 a 的值(我们将在下面的示例中看到一些fantasy-land's implementations 的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的签名有两种情况:
-
Apply 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 和Arrays 时,第一个参数是一个函数列表,即使你只想应用一个函数,也要把它放在一个数组中。
让我们看一个例子,我使用来自ramada-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); // invoke Apply#ap
const b2 = R.ap(plus3, a); // invoke R.ap
console.log(b); // Just { value: 5 }
console.log(b2); // Just { value: 5 }
理解R.lift的例子
在R.lift 的数组示例中,将一个元数为3 的函数传递给R.lift:var madd3 = R.lift((a, b, c) => a + b + c);,它如何处理三个数组[1, 2, 3], [1, 2, 3], [1]?另请注意,它不是咖喱。
实际上在source code of R.liftN(R.lift 委托给)内部,传入的函数是自动柯里化,然后它遍历值(在我们的例子中是三个数组),减少到结果:在每次迭代中,它使用 curried 函数和一个值(在我们的例子中是一个数组)调用 ap。很难用语言解释,让我们看一下代码中的等价物:
const R = require('ramda');
const madd3 = (x, y, z) => x + y + z;
// example from R.lift
const result = R.lift(madd3)([1, 2, 3], [1, 2, 3], [1]);
// this is equivalent of the calculation of 'result' above,
// R.liftN uses reduce, but the idea is the same
const result2 = R.ap(R.ap(R.ap([R.curry(madd3)], [1, 2, 3]), [1, 2, 3]), [1]);
console.log(result); // [ 3, 4, 5, 4, 5, 6, 5, 6, 7 ]
console.log(result2); // [ 3, 4, 5, 4, 5, 6, 5, 6, 7 ]
一旦理解了计算result2的表达式,例子就清楚了。
这是另一个示例,在 Apply 上使用 R.lift:
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); // invoke #ap on Apply
const sumResult2 = R.ap(R.ap(R.ap(madd3Curried, a), b), c); // invoke R.ap
const sumResult3 = R.lift(madd3)(a, b, c); // invoke R.lift, madd3 is auto-curried
console.log(sumResult); // Just { value: 6 }
console.log(sumResult2); // Just { value: 6 }
console.log(sumResult3); // Just { value: 6 }
Scott Sauyet 在 cmets 中提出的一个更好的例子(他提供了相当多的见解,我建议你阅读它们)会更容易理解,至少它为读者指明了 R.lift 计算笛卡尔积的方向Arrays.
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]
希望这会有所帮助。