Understanding lift
Thanks to the answers from Scott Sauyet and Bergi, I wrapped my head around it. In doing so, I felt there were still hoops to jump to put all the pieces together. I will document some questions I had in the journey, hope it could be of help to some.
Here's the example of R.lift
we try to understand:
var madd3 = R.lift((a, b, c) => a + b + c);
madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7]
To me, there are three questions to be answered before understanding it.
- Fantasy-land's
Apply
spec (I will refer to it asApply
) and whatApply#ap
does - Ramda's
R.ap
implementation and what doesArray
has to do with theApply
spec - What role does currying play in
R.lift
Understanding the Apply
spec
In fantasy-land, an object implements Apply
spec when it has an ap
method defined (that object also has to implement Functor
spec by defining a map
method).
The ap
method has the following signature:
ap :: Apply f => f a ~> f (a -> b) -> f b
In fantasy-land's type signature notation:
=>
declares type constraints, sof
in the signature above refers to typeApply
~>
declares method declaration, soap
should be a function declared onApply
which wraps around a value which we refer to asa
(we will see in the example below, some fantasy-land's implementations ofap
are not consistent with this signature, but the idea is the same)
Let's say we have two objects v
and u
(v = f a; u = f (a -> b)
) thus this expression is valid v.ap(u)
, some things to notice here:
v
andu
both implementApply
.v
holds a value,u
holds a function but they have the same 'interface' ofApply
(this will help in understanding the next section below, when it comes toR.ap
andArray
)- The value
a
and functiona -> b
are ignorant ofApply
, the function just transforms the valuea
. It's theApply
that puts value and function inside the container andap
that extracts them out, invokes the function on the value and puts them back in.
Understanding Ramda
's R.ap
The signature of R.ap
has two cases:
Apply f => f (a → b) → f a → f b
: This is very similar to the signature ofApply#ap
in last section, the difference is howap
is invoked (Apply#ap
vs.R.ap
) and the order of params.[a → b] → [a] → [b]
: This is the version if we replaceApply f
withArray
, remember that the value and function has to be wrapped in the same container in the previous section? That's why when usingR.ap
withArray
s, the first argument is a list of functions, even if you want to apply only one function, put it in an Array.
Let's look at one example, I'm using Maybe
from ramada-fantasy
, which implements Apply
, one inconsistency here is that Maybe#ap
's signature is: ap :: Apply f => f (a -> b) ~> f a -> f b
. Seems some other fantasy-land
implementations also follow this, however, it shouldn't affect our understanding:
const R = require('ramda');
const Maybe = require('ramda-fantasy').Maybe;
const a = Maybe.of(2);
const plus3 = Maybe.of(x => x + 3);
const b = plus3.ap(a); // invoke Apply#ap
const b2 = R.ap(plus3, a); // invoke R.ap
console.log(b); // Just { value: 5 }
console.log(b2); // Just { value: 5 }
Understanding the example of R.lift
In R.lift
's example with arrays, a function with arity of 3 is passed to R.lift
: var madd3 = R.lift((a, b, c) => a + b + c);
, how does it work with the three arrays [1, 2, 3], [1, 2, 3], [1]
? Also note that it's not curried.
Actually inside source code of R.liftN
(which R.lift
delegates to), the function passed in is auto-curried, then it iterates through the values (in our case, three arrays), reducing to a result: in each iteration it invokes ap
with the curried function and one value (in our case, one array). It's hard to explain in words, let's see the equivalent in code:
const R = require('ramda');
const Maybe = require('ramda-fantasy').Maybe;
const madd3 = (x, y, z) => x + y + z;
// example from R.lift
const result = R.lift(madd3)([1, 2, 3], [1, 2, 3], [1]);
// this is equivalent of the calculation of 'result' above,
// R.liftN uses reduce, but the idea is the same
const result2 = R.ap(R.ap(R.ap([R.curry(madd3)], [1, 2, 3]), [1, 2, 3]), [1]);
console.log(result); // [ 3, 4, 5, 4, 5, 6, 5, 6, 7 ]
console.log(result2); // [ 3, 4, 5, 4, 5, 6, 5, 6, 7 ]
Once the expression of calculating result2
is understood, the example will become clear.
Here's another example, using R.lift
on Apply
:
const R = require('ramda');
const Maybe = require('ramda-fantasy').Maybe;
const madd3 = (x, y, z) => x + y + z;
const madd3Curried = Maybe.of(R.curry(madd3));
const a = Maybe.of(1);
const b = Maybe.of(2);
const c = Maybe.of(3);
const sumResult = madd3Curried.ap(a).ap(b).ap(c); // invoke [ap](ap.md) on Apply
const sumResult2 = R.ap(R.ap(R.ap(madd3Curried, a), b), c); // invoke R.ap
const sumResult3 = R.lift(madd3)(a, b, c); // invoke R.lift, madd3 is auto-curried
console.log(sumResult); // Just { value: 6 }
console.log(sumResult2); // Just { value: 6 }
console.log(sumResult3); // Just { value: 6 }
A better example suggested by Scott Sauyet in the comments (he provides quite some insights, I suggest you read them) would be easier to understand, at least it points the reader to the direction that R.lift
calculates the Cartesian product for Array
s.
var madd3 = R.lift((a, b, c) => a + b + c);
madd3([100, 200], [30, 40, 50], [6, 7]); //=> [136, 137, 146, 147, 156, 157, 236, 237, 246, 247, 256, 257]
Hope this helps.
Keywords: Functional Programming, Monads