为什么我可以将函数传递给提升的R.divide?

How come I can pass functions to a lifted R.divide?

鉴于以下情况:

var average = R.lift(R.divide)(R.sum, R.length)

为什么它可以作为 average 的无点实现?我不明白为什么我可以传递 R.sumR.length 当它们是函数时,因此,我无法将提升的 R.divide 映射到函数 R.sumR.length 与以下示例不同:

var sum3 = R.curry(function(a, b, c) {return a + b + c;});
R.lift(sum3)(xs)(ys)(zs)

在上述情况下,xsyszs 中的值在非确定性上下文中求和,在这种情况下,提升函数应用于给定计算上下文。

进一步阐述,我理解应用提升函数就像对每个参数连续使用 R.ap。两行的计算结果相同:

R.ap(R.ap(R.ap([tern], [1, 2, 3]), [2, 4, 6]), [3, 6, 8])
R.lift(tern)([1, 2, 3], [2, 4, 6], [3, 6, 8])

查看它说的文档:

"lifts" a function of arity > 1 so that it may "map over" a list, Function or other object that satisfies the FantasyLand Apply spec.

至少对我来说,这似乎不是一个非常有用的描述。我正在尝试建立关于 lift 用法的直觉。希望有人能提供一下。

首先很酷的是a -> b可以支持map。是的,函数是仿函数!

让我们考虑map的类型:

map :: Functor f => (b -> c) -> f b -> f c

让我们用 Array 替换 Functor f => f 来给我们一个具体的类型:

map :: (b -> c) -> Array b -> Array c

这次我们把Functor f => f换成Maybe

map :: (b -> c) -> Maybe b -> Maybe c

相关性很明显。让我们将 Functor f => f 替换为 Either a,以测试二进制类型:

map :: (b -> c) -> Either a b -> Either a c

我们经常将 ab 的函数类型表示为 a -> b,但这实际上只是 Function a b 的糖分。让我们使用长格式并将上面签名中的 Either 替换为 Function:

map :: (b -> c) -> Function a b -> Function a c

因此,映射到一个函数给我们一个函数,它将 b -> c 函数应用于原始函数的 return 值。我们可以使用 a -> b 糖重写签名:

map :: (b -> c) -> (a -> b) -> (a -> c)

注意到什么了吗? compose 的类型是什么?

compose :: (b -> c) -> (a -> b) -> a -> c

所以 compose 只是 map 专用于 Function 类型!

第二个很酷的事情是 a -> b 可以支持 Fantasy Land 规范中的 ap. Functions are also applicative functors! These are known as Applys。

让我们考虑ap的类型:

ap :: Apply f => f (b -> c) -> f b -> f c

让我们将 Apply f => f 替换为 Array:

ap :: Array (b -> c) -> Array b -> Array c

现在,Either a

ap :: Either a (b -> c) -> Either a b -> Either a c

现在,Function a

ap :: Function a (b -> c) -> Function a b -> Function a c

什么是Function a (b -> c)?这有点令人困惑,因为我们混合了两种样式,但它是一个接受类型 a 值的函数,而 return 是一个从 bc 的函数。让我们使用 a -> b 风格重写:

ap :: (a -> b -> c) -> (a -> b) -> (a -> c)

任何支持 mapap 的类型都可以被“提升”。我们来看看lift2:

lift2 :: Apply f => (b -> c -> d) -> f b -> f c -> f d

记住Function a满足Apply的要求,所以我们可以把Apply f => f换成Function a:

lift2 :: (b -> c -> d) -> Function a b -> Function a c -> Function a d

哪个写的更清楚:

lift2 :: (b -> c -> d) -> (a -> b) -> (a -> c) -> (a -> d)

让我们重温一下您的初始表达式:

//    average :: Number -> Number
const average = lift2(divide, sum, length);

average([6, 7, 8]) 是做什么的? a ([6, 7, 8]) 被赋予 a -> b 函数 (sum),生成 b (21)。 a 也被赋予 a -> c 函数 (length),生成 c (3)。现在我们有一个 b 和一个 c 我们可以将它们提供给 b -> c -> d 函数 (divide) 以产生一个 d (7 ), 这是最终结果。

所以,因为函数类型可以支持mapap,我们可以免费得到converge(通过liftlift2lift3)。我实际上想从 Ramda 中删除 converge,因为没有必要。


请注意,我有意避免在此答案中使用 R.lift。由于决定支持任何 arity 的功能,它具有无意义的类型签名和复杂的实现。另一方面,Sanctuary 特定于 arity 的提升函数具有清晰的类型签名和简单的实现。

由于我很难理解同样的问题,所以我决定看一下 Ramda 的源代码。以后会写一篇关于这个的博文。同时——我对 Ramda 的 lift 如何一步步工作做了一个评论要点。

来自:https://gist.github.com/philipyoungg/a0ab1efff1a9a4e486802a8fb0145d9e

// Let's make an example function that takes an object and return itself.
// 1. Ramda's lift level
lift(zipObj)(keys, values)({a: 1}) // returns {a: 1}

// this is how lift works in the background
module.exports = _curry2(function liftN(arity, fn) {
  var lifted = curryN(arity, fn);
  return curryN(arity, function() {
    return _reduce(ap, map(lifted, arguments[0]), Array.prototype.slice.call(arguments, 1)); // found it. let's convert no 1 to no 2
  });
});

// 2. Ramda's reduce level
reduce(ap, map(zipObj, keys))([values])
// first argument is the function, second argument is initial value, and the last one is lists of arguments. If you don't understand how reduce works, there's a plenty of resources on the internet

// 3. Ramda's ap level
ap(map(zipObj, keys), values)

// how ap works in the background
module.exports = _curry2(function ap(applicative, fn) {
  return (
    typeof applicative.ap === 'function' ?
      applicative.ap(fn) :
    typeof applicative === 'function' ? // 
      function(x) { return applicative(x)(fn(x)); } : // because the first argument is a function, ap return this.
    // else
      _reduce(function(acc, f) { return _concat(acc, map(f, fn)); }, [], applicative)
  );
});

// 4. Voilà. Here's the final result.
map(zipObj, keys)({a: 1})(values({a: 1}))

// Hope it helps you and everyone else!