28

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

电梯

电梯N

这是给定的示例:

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]

所以结果的第一个数字很简单,a, b, 和c, 都是每个数组的第一个元素。第二个对我来说并不容易理解。参数是每个数组的第二个值(2、2、未定义)还是第一个数组的第二个值和第二个和第三个数组的第一个值?

即使不考虑这里发生的事情的顺序,我也没有真正看到价值。如果我在没有lift先执行它的情况下执行它,我最终会concat得到作为字符串生成的数组。这似乎有点像flatMap,但我似乎无法遵循它背后的逻辑。

4

3 回答 3

41

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

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

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

这对三个数字起作用。但是,如果您有装有数字的容器怎么办?也许我们有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. Ramdaap以类似于将函数应用于列表叉积中的元组的方式实现列表,因此我们也可以这样做:

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

我就是这么想的lift。它采用在某些值级别上工作的函数,并将其提升到在这些值的容器级别上工作的函数。

于 2016-04-12T15:39:42.810 回答
19

多亏了 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-landApply规格(我将其称为Apply)和Apply#ap什么
  2. Ramda 的R.ap实现以及ArrayApply规范有什么关系
  3. 柯里化在其中起什么作用R.lift

了解Apply规范

在幻想世界中,对象在定义了方法Apply时实现规范ap(该对象还必须Functor通过定义map方法来实现规范)。

ap方法具有以下签名:

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

幻想世界的类型签名符号中

  • =>声明类型约束,所以f在上面的签名中指的是类型Apply
  • ~>声明方法声明,所以ap应该是一个声明的函数,Apply它包含一个我们称为的值a(我们将在下面的示例中看到,一些幻想世界的实现ap此签名不一致,但想法是相同的)

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

  • v并且u都实现了Apply. v持有一个值,u持有一个函数,但它们具有相同的“接口” Apply(这将有助于理解下面的下一节,当谈到R.apand时Array
  • a和函数a -> b是无知的Apply,函数只是对值进行变换a。它将Apply值和函数放入容器中,然后ap将它们提取出来,在值上调用函数并将它们放回容器中。

了解RamdaR.ap

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

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

让我们看一个例子,我正在使用Maybefrom ramada-fantasy,它实现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带有数组的示例中,将一个 arity 为 3 的函数传递给R.lift: var madd3 = R.lift((a, b, c) => a + b + c);,它如何与三个数组一起工作[1, 2, 3], [1, 2, 3], [1]?另请注意,它不是咖喱。

实际上在(委托给)的源代码中R.liftNR.lift,传入的函数是auto-curried,然后它遍历值(在我们的例子中,三个数组),减少到一个结果:在每次迭代中它调用apcurried 函数和一个值(在我们的例子中,一个数组)。很难用语言解释,让我们看一下代码中的等价物:

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,例子就会变得清晰。

这是另一个示例,使用R.lifton 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);  // 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 在评论中建议的一个更好的例子(他提供了很多见解,我建议你阅读它们)会更容易理解,至少它为读者指明了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]

希望这可以帮助。

于 2017-06-01T11:49:15.700 回答
8

lift/liftN将普通函数“提升”到 Applicative 上下文中。

// 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).

于 2016-04-11T20:45:32.693 回答