Given the following:
var average = R.lift(R.divide)(R.sum, R.length)
How come this works as a pointfree implementation of average
? I don't understand why I can pass R.sum
and R.length
when they are functions and therefore, I cannot map the lifted R.divide
over the functions R.sum
and R.length
unlike in the following example:
var sum3 = R.curry(function(a, b, c) {return a + b + c;});
R.lift(sum3)(xs)(ys)(zs)
In the above case the values in xs
, ys
and zs
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 R.ap
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 lift
. I hope someone can provide that.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With