Chad Chad - 1 year ago 34
Javascript Question

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

Given the following:

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

How come this works as a pointfree implementation of
? I don't understand why I can pass
when they are functions and therefore, I cannot map the lifted
over the functions
unlike in the following example:

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

In the above case the values in
are summed in a non deterministic context, in which case the lifted function is applied to the values in the given computational context.

Expounding further, I understand that applying a lifted function is like using
consecutively to each argument. Both lines evaluate to the same output:

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])

Checking the documentation it says:

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

And that doesn't seem like a very useful description at least for me. I'm trying to build an intuition regarding the usage of
. I hope someone can provide that.

Answer Source

The first cool thing is that a -> b can support map. Yes, functions are functors!

Let's consider the type of map:

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

Let's replace Functor f => f with Array to give us a concrete type:

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

Let's replace Functor f => f with Maybe this time:

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

The correlation is clear. Let's replace Functor f => f with Either a, to test a binary type:

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

We often represent the type of a function from a to b as a -> b, but that's really just sugar for Function a b. Let's use the long form and replace Either in the signature above with Function:

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

So, mapping over a function gives us a function which will apply the b -> c function to the original function's return value. We could rewrite the signature using the a -> b sugar:

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

Notice anything? What is the type of compose?

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

So compose is just map specialized to the Function type!

The second cool thing is that a -> b can support ap. Functions are also applicative functors! These are known as Applys in the Fantasy Land spec.

Let's consider the type of ap:

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

Let's replace Apply f => f with Array:

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

Now, with Either a:

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

Now, with Function a:

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

What is Function a (b -> c)? It's a bit confusing because we're mixing the two styles, but it's a function that takes a value of type a and returns a function from b to c. Let's rewrite using the a -> b style:

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

Any type which supports map and ap can be "lifted". Let's take a look at lift2:

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

Remember that Function a satisfies the requirements of Apply, so we can replace Apply f => f with Function a:

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

Which is more clearly written:

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

Let's revisit your initial expression:

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

What does average([6, 7, 8]) do? The a ([6, 7, 8]) is given to the a -> b function (sum), producing a b (21). The a is also given to the a -> c function (length), producing a c (3). Now that we have a b and a c we can feed them to the b -> c -> d function (divide) to produce a d (7), which is the final result.

So, because the Function type can support map and ap, we get converge at no cost (via lift, lift2, and lift3). I'd actually like to remove converge from Ramda as it isn't necessary.

Note that I intentionally avoided using R.lift in this answer. It has a meaningless type signature and complex implementation due to the decision to support functions of any arity. Sanctuary's arity-specific lifting functions, on the other hand, have clear type signatures and trivial implementations.