7

鉴于以下情况:

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

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

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

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

进一步阐述,我理解应用提升函数就像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])

检查它说的文档:

“提升” arity > 1 的函数,以便它可以“映射”满足 FantasyLand Apply 规范的列表、函数或其他对象。

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

4

2 回答 2

13

第一个很酷的地方就是a -> b可以支持map。是的,函数就是函子!

让我们考虑以下类型map

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

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

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

让我们Functor f => fMaybe这个时间替换:

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

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

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

我们经常将函数的类型从ato表示b为 as a -> b,但这实际上只是 for 的糖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。函数也是应用函子!这些在 Fantasy Land 规范中称为Apply 。

让我们考虑以下类型ap

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

让我们替换Apply f => fArray

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并返回一个函数 from bto ca -> 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 => fFunction 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) ,产生( )。也赋予函数 ( ) ,产生( )。现在我们有了 a和 a,我们可以将它们提供给函数 ( ) 以生成( ),这是最终结果。[6, 7, 8]a -> bsumb21aa -> clengthc3bcb -> c -> ddivided7

所以,因为 Function 类型可以支持mapand ap,所以我们可以converge免费获得(通过liftlift2lift3)。我实际上想converge从 Ramda 中删除,因为它没有必要。


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

于 2016-09-17T13:10:19.183 回答
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!
于 2016-12-31T19:18:39.380 回答