【问题标题】:Can't wrap my head around "lift" in Ramda.js无法在 Ramda.js 中“提升”我的头
【发布时间】:2016-08-02 04:47:38
【问题描述】:

查看 Ramda.js 的源代码,特别是“提升”功能。

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)还是第一个数组的第二个值和第二个和第三个数组的第一个值?

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

【问题讨论】:

标签: javascript functional-programming ramda.js lifting


【解决方案1】:

多亏了 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 规范

在幻想世界中,当一个对象定义了一个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 implementationsap 和这个签名不一致,但是思路是一样的)

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

  • vu 都实现了 Applyv 持有一个值,u 持有一个函数,但它们具有与Apply 相同的“接口”(这将有助于理解下面的下一节,涉及到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 时,第一个参数是一个函数列表,即使你只想应用一个函数,也要把它放在一个数组中。

让我们看一个例子,我使用来自ramada-fantasyMaybe,它实现了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.liftNR.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]

希望这会有所帮助。

【讨论】:

  • 这太棒了。我将制作一些 cmets,希望能对棘手的问题进行一些澄清。但首先,有一个建议:文档中的示例很难理解。像这样的一个可能会有所帮助:madd3([100, 200], [30, 40, 50], [6, 7]); //=> [136, 137, 146, 147, 156, 157, 236, 237, 246, 247, 256, 257]。 (并且欢迎 PR 来修复文档。)
  • Maybe#ap 实际上确实具有 FantasyLand 规范要求的相同签名。本质上,apf a 类型对象的方法。 Ramda 的ap 函数 接受(a -> b) 转换和ApplyX a 并返回ApplyX b,用于Apply 的某些实现,称为ApplyX。 (这可能比签名中使用的f 更清晰一些。)在许多情况下,Ramda 的函数委托给该对象的方法。
  • 虽然从实现上看似乎如此,但Apply 是一种情况而Array 是另一种情况并不是真的。考虑这一点的更简洁的方法是Arrays 可以被认为是Applys(和Functors 等),但由于它们没有内置的ap 方法,Ramda 提供一个给他们。 Ramda 还为Arrays 提供了一个适当的map 函数,因为Array.prototype 中的一个函数不遵循适当的规则。 Ramda 在各个地方为Objects 和Functions 做了类似的事情。
  • 最后 (?) 请注意,函数在进入的过程中会被柯里化,因为每次调用 ap 只处理一个参数。如果我们从 (x) => (y) => (z) => x + y + z 这样的函数开始,那就没有必要了。
  • 感谢 Scott Sauyet 的解释!根据您的建议,我创建了一个 PR,用于更新 liftliftN 的示例。
【解决方案2】:

Bergi 的回答很棒。但另一种思考方式是更具体一点。 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。 Ramda 为列表实现ap 的方式类似于将函数应用于列表叉积中的元组,因此我们也可以这样做:

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

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

【讨论】:

  • 啊,所以如果我理解正确的话,它会“学习”如何通过实现.ap 来使用容器。我还在ramda-fantasy 存储库中看到了Just.prototype.ap = function(m) { return m.map(this.value); };ap 是典型的函数式编程,还是 ramda/javascript 处理 lift 的方式?
  • 我发现这张照片对我很有帮助:drboolean.gitbooks.io/mostly-adequate-guide/content/images/…
  • ap 或类似的东西在函数式编程语言/库中很常见。根据这些 FantasyLand 规范定义的功能实现lift 意味着它将与任何符合 FL 的 Applicative 类型一起使用。这是一个不错的胜利。
  • 我对此仍然有些模糊,但我准确地写出了生成 madd3 示例的结果的确切原因:var madd3 = R.lift((a, b, c) => a + b + c); madd3([3, 7], [9, 5], [4, 6]); 导致 [16 , 18, 12, 14, 20, 22, 16, 18] 计算如下:3 + 9 + 4, 3 + 9 + 6, 3 + 5 + 4, 3 + 5 + 6, 7 + 9 + 4, 7 + 9 + 6, 7 + 5 + 4, 7 + 5 + 6
【解决方案3】:

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)

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-06-06
  • 2016-04-06
  • 1970-01-01
  • 2012-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多