无法在 Ramda.js 中绕过 "lift"

Can't wrap my head around "lift" in Ramda.js

查看 Ramda.js 的源代码,特别是 "lift" 函数。

lift

liftN

这是给定的例子:

var madd3 = R.lift(R.curry((a, b, c) => a + b + c));

madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7]

所以结果的第一个数就简单了,abc都是每个数组的第一个元素。第二个对我来说不太容易理解。参数是每个数组的第二个值 (2, 2, undefined) 还是第一个数组的第二个值以及第二个和第三个数组的第一个值?

即使不考虑这里发生的事情的顺序,我也看不出真正的价值。如果我在没有先 lifting 的情况下执行此操作,我将以 concat 作为字符串的数组结束。这看起来有点像 flatMap 但我似乎无法理解它背后的逻辑。

lift/liftN "lifts" 将普通函数转换为应用上下文。

// lift1 :: (a -> b) -> f a -> f b
// lift1 :: (a -> b) -> [a] -> [b]
function lift1(fn) {
    return function(a_x) {
        return R.ap([fn], a_x);
    }
}

现在ap(f (a->b) -> f a -> f b)的类型也不太好理解,不过列表的例子应该可以理解。

这里有趣的是你传入一个列表并返回一个列表,所以你可以重复应用它,只要第一个列表中的函数具有正确的类型:

// lift2 :: (a -> b -> c) -> f a -> f b -> f c
// lift2 :: (a -> b -> c) -> [a] -> [b] -> [c]
function lift2(fn) {
    return function(a_x, a_y) {
        return R.ap(R.ap([fn], a_x), a_y);
    }
}

和您在示例中隐式使用的 lift3 的工作方式相同 - 现在使用 ap(ap(ap([fn], a_x), a_y), a_z)

Bergi 的回答很棒。但另一种思考这个问题的方法是更具体一点。 Ramda 确实需要在其文档中包含一个非列表示例,因为列表并没有真正捕捉到这一点。

让我们看一个简单的函数:

var add3 = (a, b, c) => a + b + c;

这对三个数字进行运算。但是,如果您有 containers 保存数字怎么办?也许我们有 Maybes。我们不能简单地将它们加在一起:

const Just = Maybe.Just, Nothing = Maybe.Nothing;
add3(Just(10), Just(15), Just(17)); //=> ERROR!

(好的,这是 Javascript,它实际上不会在这里抛出错误,只是尝试连接它不应该的东西......但它绝对不会做你想要的!)

如果我们能将该功能提升到容器级别,那将使我们的生活更轻松。 Bergi 指出的 lift3 是在 Ramda 中用 liftN(3, fn) 和一个注释 lift(fn) 实现的,它只使用所提供函数的元数。所以,我们可以这样做:

const madd3 = R.lift(add3);
madd3(Just(10), Just(15), Just(17)); //=> Just(42)
madd3(Just(10), Nothing(), Just(17)); //=> Nothing()

但是这个提升的函数并不知道关于我们容器的任何具体信息,只知道它们实现了 ap。 Ramda 以类似于将函数应用于列表叉积中的元组的方式为列表实现 ap,因此我们也可以这样做:

madd3([100, 200], [30, 40], [5, 6, 7]);
//=> [135, 136, 137, 145, 146, 147, 235, 236, 237, 245, 246, 247]

这就是我对 lift 的看法。它需要一个在某些值级别工作的函数,并将其提升到一个在这些值的容器级别工作的函数。

感谢 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]

对我来说,在理解它之前需要回答三个问题

  1. Fantasy-land's Apply 规范(我将其称为 Apply)以及 Apply#ap 的作用
  2. Ramda 的 R.ap implementation and what does ArrayApply 规范有关
  3. 柯里化在R.lift
  4. 中扮演什么角色

了解 Apply 规范

在幻想世界中,对象通过定义 map 方法来实现 Apply spec when it has an ap method defined (that object also has to implement Functor 规范。

ap 方法具有以下签名:

ap :: Apply f => f a ~> f (a -> b) -> f b

fantasy-land's type signature notation中:

  • =>声明了类型约束,所以上面签名中的f指的是类型Apply
  • ~> 声明了 method 声明,因此 ap 应该是在 Apply 上声明的函数,它环绕着我们引用的值as a(我们会在下面的例子中看到,ap中的一些fantasy-land's implementations和这个签名不一致,但是思路是一样的)

假设我们有两个对象 vu (v = f a; u = f (a -> b)) 因此这个表达式是有效的 v.ap(u),这里需要注意一些事情:

  • vu 都实现了 Applyv 包含一个值,u 包含一个函数,但它们具有与 Apply 相同的 'interface'(这将有助于理解下面的下一节,当谈到 R.apArray)
  • a和函数a -> b不知道Apply,函数只是转换值aApply 将值和函数放入容器中,ap 将它们提取出来,调用值上的函数并将它们放回容器中。

了解 RamdaR.ap

R.ap的签名有两种情况:

  1. Apply f => f (a → b) → f a → f b:这与上一节中Apply#ap的签名非常相似,区别在于ap的调用方式(Apply#ap vs. R.ap) 和参数的顺序。
  2. [a → b] → [a] → [b]:如果我们将Apply f替换为Array,这就是版本,还记得上一节中值和函数必须包装在同一个容器中吗?这就是为什么当 R.apArrays 一起使用时,第一个参数是一个函数列表,即使你只想应用一个函数,把它放在一个数组中。

让我们看一个例子,我使用Maybe来自ramda-fantasy, which implements Apply, one inconsistency here is that 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.liftvar madd3 = R.lift((a, b, c) => a + b + c);,它如何与三个数组一起工作[1, 2, 3], [1, 2, 3], [1]?另请注意,它不是咖喱。

实际上在 source code of R.liftN 内部(R.lift 委托),传入的函数是 auto-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 在评论中建议的一个更好的例子(他提供了很多见解,我建议你阅读它们)会更容易理解,至少它指出了 reader 的方向 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]

希望对您有所帮助。