不能把我的头包起来#34;抬起"在Ramda.js

时间:2016-04-11 20:26:11

标签: javascript functional-programming ramda.js lifting

查看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,未定义)还是第一个数组的第二个值以及第二个和第三个数组的第一个值?

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

3 个答案:

答案 0 :(得分:32)

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

让我们采取一个简单的功能:

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

这适用于三个数字。但是如果你有容器持有数字怎么办?也许我们有Maybe个。我们不能简单地将它们加在一起:

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

(好吧,这是Javascript,它实际上不会在这里引发错误,只是尝试连接它不应该的东西......但它肯定不能做你想要的!)

如果我们能够将该功能提升到容器的水平,它将使我们的生活更轻松。 Bergi指出lift3的内容在Ramda中使用liftN(3, fn)实现,而光泽lift(fn)只使用所提供函数的arity。所以,我们可以这样做:

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的看法。它需要一个在某些值的水平上起作用的函数,并将其提升到一个在这些值的容器级别起作用的函数。

答案 1 :(得分:10)

感谢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' Apply规范(我将其称为Apply)以及Apply#ap做什么
  2. Ramda的R.ap实施以及ArrayApply规范
  3. 的关系
  4. curry在R.lift
  5. 中扮演什么角色

    了解Apply规范

    在fantasy-land中,对象在定义ap方法时实现Apply规范(该对象还必须通过定义map方法来实现Functor规范)。

    ap方法具有以下签名:

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

    fantasy-land's type signature notation中:

    • =>声明类型约束,因此上面签名中的f引用类型Apply
    • ~>声明了方法声明,因此ap应该是Apply上声明的函数,它包含一个我们称为{{1}的值(我们将在下面的示例中看到,a的某些fantasy-land's implementations与此签名不一致,但这个想法是相同的)

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

    • v.ap(u)v都实施uApply包含一个值,v包含一个函数,但它们具有相同的界面' u的{​​{1}} ApplyR.ap
    • 有助于理解下一节
    • Array和函数a不知道a -> b,该函数只会转换值Apply。它是将值和函数放在容器中的a和将它们提取出来的Apply,调用值上的函数并将它们放回去。

    了解ap' s Ramda

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

    1. R.ap:这与上一节Apply f => f (a → b) → f a → f b的签名非常相似,不同之处在于Apply#apap的对比方式Apply#ap )和params的顺序。
    2. R.ap:如果我们将[a → b] → [a] → [b]替换为Apply f,这是版本,请记住,值和函数必须包含在上一节中的同一容器中吗?这就是为什么当ArrayR.ap一起使用时,第一个参数是一个函数列表,即使你只想应用一个函数,也把它放在一个数组中。
    3. 让我们看一个例子,我使用ramada-fantasy中的Array来实现Maybe,这里的一个不一致是Maybe#ap& #39;签名是:Apply。似乎其他一些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的数组示例中,将arity为3的函数传递给R.liftR.lift,它如何与三个数组{{1}一起使用}}?另请注意,它并非咖喱。

      实际上在source code of R.liftN内(var madd3 = R.lift((a, b, c) => a + b + c);代表),传入的函数是自动咖喱,然后它遍历值(在我们的例子中,三个数组) ),减少到一个结果:在每次迭代中,它使用curried函数和一个值(在我们的例子中,一个数组)调用[1, 2, 3], [1, 2, 3], [1]。用文字解释很难,让我们看看代码中的等价物:

      R.lift

      一旦理解了计算ap的表达式,示例就会变得清晰。

      这是另一个例子,在const R = require('ramda'); const Maybe = require('ramda-fantasy').Maybe; 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.lift

      Scott Sauyet在评论中提出的一个更好的例子(他提供了一些见解,我建议你阅读它们)会更容易理解,至少它指出了读者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 } 的产品。

      R.lift

      希望这有帮助。

答案 2 :(得分:7)

lift / liftN“将普通功能提升为适用语境。

// 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);
    }
}

现在apf (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)