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

时间:2016-09-17 10:47:00

标签: javascript functional-programming ramda.js lifting

鉴于以下内容:

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

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

R.length

在上面的例子中,var sum3 = R.curry(function(a, b, c) {return a + b + c;}); R.lift(sum3)(xs)(ys)(zs) xsys中的值在非确定性上下文中求和,在这种情况下,提升函数应用于给定计算中的值上下文。

进一步说明,我理解应用提升函数就像连续地对每个参数使用zs。这两行评估为相同的输出:

R.ap

检查文档说明:

  

"升降机" arity的功能> 1,以便它可以"映射"满足FantasyLand Apply规范的列表,函数或其他对象。

至少对我而言,这似乎不是一个非常有用的描述。我试图建立一个关于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]) 用法的直觉。我希望有人可以提供。

2 个答案:

答案 0 :(得分:13)

第一个很酷的事情是a -> b可以支持map。是的,功能是仿函数!

让我们考虑一下map

的类型
map :: Functor f => (b -> c) -> f b -> f c

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

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函数应用于原始函数的返回值。我们可以使用a -> b糖重写签名:

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

注意什么? compose的类型是什么?

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

所以compose只是map专门用于函数类型!

第二个很酷的事情是a -> b可以支持ap。函数也是 applicative 仿函数!这些被称为Fantasy Land规范中的Apply

让我们考虑一下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类型值的函数,并返回从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 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),生成b21)。 a函数(a -> c)也会length,并生成c3)。现在我们有bc,我们可以将它们提供给b -> c -> d函数(divide)以生成d7 ),这是最终的结果。

因此,因为函数类型可以支持mapap,所以我们免费获得converge(通过liftlift2和{{ 3}})。我实际上想从Ramda删除converge,因为没有必要。

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

答案 1 :(得分:0)

由于我很难理解同样的问题,所以我决定先看看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!