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.