29

Looking at the source for Ramda.js, specifically at the "lift" function.

lift

liftN

Here's the given example:

var madd3 = R.lift(R.curry((a, b, c) => a + b + c));

madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7]

So the first number of the result is easy, a, b, and c, are all the first elements of each array. The second one isn't as easy for me to understand. Are the arguments the second value of each array (2, 2, undefined) or is it the second value of the first array and the first values of the second and third array?

Even disregarding the order of what's happening here, I don't really see the value. If I execute this without lifting it first I will end up with the arrays concatenated as strings. This appears to sort of be working like flatMap but I can't seem to follow the logic behind it.

diplosaurus
  • 2,538
  • 5
  • 25
  • 53
  • 4
    You will likely benefit from reading about [Applicative Functors](https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch10.html) – Mulan Apr 12 '16 at 14:01

3 Answers3

44

Bergi's answer is great. But another way to think about this is to get a little more specific. Ramda really needs to include a non-list example in its documentation, as lists don't really capture this.

Lets take a simple function:

var add3 = (a, b, c) => a + b + c;

This operates on three numbers. But what if you had containers holding numbers? Perhaps we have Maybes. We can't simply add them together:

const Just = Maybe.Just, Nothing = Maybe.Nothing;
add3(Just(10), Just(15), Just(17)); //=> ERROR!

(Ok, this is Javascript, it will not actually throw an error here, just try to concatenate thing it shouldn't... but it definitely doesn't do what you want!)

If we could lift that function up to the level of containers, it would make our life easier. What Bergi pointed out as lift3 is implemented in Ramda with liftN(3, fn), and a gloss, lift(fn) that simply uses the arity of the function supplied. So, we can do:

const madd3 = R.lift(add3);
madd3(Just(10), Just(15), Just(17)); //=> Just(42)
madd3(Just(10), Nothing(), Just(17)); //=> Nothing()

But this lifted function doesn't know anything specific about our containers, only that they implement ap. Ramda implements ap for lists in a way similar to applying the function to the tuples in the crossproduct of the lists, so we can also do this:

madd3([100, 200], [30, 40], [5, 6, 7]);
//=> [135, 136, 137, 145, 146, 147, 235, 236, 237, 245, 246, 247]

That is how I think about lift. It takes a function that works at the level of some values and lifts it up to a function that works at the level of containers of those values.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 2
    Ahh, so it "learns" how to work with the containers through their implementation of `.ap`, if I understand correctly. I also see `Just.prototype.ap = function(m) { return m.map(this.value); };` in the `ramda-fantasy` repo. Is `ap` a typical functional programming thing or is that just how ramda/javascript handles `lift`? – diplosaurus Apr 12 '16 at 20:09
  • 1
    I found this picture which definitely helped me: https://drboolean.gitbooks.io/mostly-adequate-guide/content/images/functormap.png – diplosaurus Apr 12 '16 at 22:47
  • `ap` or something like it is common in functional programming languages / libraries. Implementing `lift` in terms of functions defined by these FantasyLand specifications means that it will work with any FL-compliant Applicative types. And that is a nice win. – Scott Sauyet Apr 13 '16 at 00:59
  • 1
    I'm still a little fuzzy on this, but I wrote out exactly what happens to generate the results of the `madd3` example: `var madd3 = R.lift((a, b, c) => a + b + c);` `madd3([3, 7], [9, 5], [4, 6]); ` Which results in _[16, 18, 12, 14, 20, 22, 16, 18]_ calculated like this: 3 + 9 + 4, 3 + 9 + 6, 3 + 5 + 4, 3 + 5 + 6, 7 + 9 + 4, 7 + 9 + 6, 7 + 5 + 4, 7 + 5 + 6 – hsrob May 26 '16 at 23:54
22

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.

  1. Fantasy-land's Apply spec (I will refer to it as Apply) and what Apply#ap does
  2. Ramda's R.ap implementation and what does Array has to do with the Apply spec
  3. 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, so f in the signature above refers to type Apply
  • ~> declares method declaration, so ap should be a function declared on Apply which wraps around a value which we refer to as a (we will see in the example below, some fantasy-land's implementations of ap 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 and u both implement Apply. v holds a value, u holds a function but they have the same 'interface' of Apply (this will help in understanding the next section below, when it comes to R.ap and Array)
  • The value a and function a -> b are ignorant of Apply, the function just transforms the value a. It's the Apply that puts value and function inside the container and ap 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:

  1. Apply f => f (a → b) → f a → f b: This is very similar to the signature of Apply#ap in last section, the difference is how ap is invoked (Apply#ap vs. R.ap) and the order of params.
  2. [a → b] → [a] → [b]: This is the version if we replace Apply f with Array, remember that the value and function has to be wrapped in the same container in the previous section? That's why when using R.ap with Arrays, 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 ramda-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 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 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 Arrays.

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.

customcommander
  • 17,580
  • 5
  • 58
  • 84
Dapeng Li
  • 3,282
  • 1
  • 24
  • 18
  • 5
    This is excellent. I will make a few comments that I hope will add some clarifications about tricky points. But first, one suggestion: The example from the docs is terrible for understanding. One like this would probably help: `madd3([100, 200], [30, 40, 50], [6, 7]); //=> [136, 137, 146, 147, 156, 157, 236, 237, 246, 247, 256, 257]`. (And a PR to fix the documentation would be welcome.) – Scott Sauyet Jun 04 '17 at 01:07
  • `Maybe#ap` actually does have the same signature as required by the FantasyLand spec. Essentially, `ap` is a **method** of objects of the type `f a`. Ramda's `ap` **function** takes an `(a -> b)` transformation and an `ApplyX a` and gives you back an `ApplyX b`, for some implementation of `Apply` called `ApplyX`. (This might be a little clearer than the `f` used in the signature.) Ramda's function delegates to the method of that object in many cases. – Scott Sauyet Jun 04 '17 at 01:18
  • 2
    Although it seems so from the implementation, it's not really true that `Apply` is one case and `Array` another. A cleaner way to think about this is that `Array`s can be thought of as `Apply`s (and `Functor`s, etc.), but since they don't have built-in `ap` methods, Ramda supplies one for them. Ramda also supplies an appropriate `map` function for `Array`s, because one in `Array.prototype` doesn't follow the appropriate rules. Ramda does similar things in various places for `Object`s and `Function`s as well. – Scott Sauyet Jun 04 '17 at 01:23
  • Finally (?) note that the function gets curried on its way in because each call to `ap` deals with only one argument. It would be unnecessary if we were to start with a function like `(x) => (y) => (z) => x + y + z`. – Scott Sauyet Jun 04 '17 at 01:32
  • Thanks for the explanations Scott Sauyet! I've created a PR for updating the samples for `lift` and `liftN` based on your suggestion. – Dapeng Li Jun 05 '17 at 12:59
  • @ScottSauyet I'm not sure that `Maybe` in `ramda-fantasy` is consistent with fantasy-land's spec. In the example of `a.ap(b)`, [fantasy-land says](https://github.com/fantasyland/fantasy-land#apply) `a` should be an `Apply` of value while `b` should be an `Apply` of function; however, the [`Maybe` of `ramda-fantasy`](https://github.com/ramda/ramda-fantasy/blob/v0.8.0/src/Maybe.js#L75) requires `a` be a `Maybe` of function and `b` be a `Maybe` of value. Am I missing something? – Dapeng Li Jun 05 '17 at 13:06
  • @ScottSauyet, you said the `map` function in Ramda is more appropriate than the `map` in `Array.prototype`. I see one difference between them is `Array.prototype.map` will skip unset values, while `R.map` would not. Is that what you were referring to? Thanks! – Dapeng Li Jun 05 '17 at 13:10
  • You're right about R-F Maybe and `ap`. I'd forgotten that FL has [changed the order](https://github.com/fantasyland/fantasy-land/pull/145) of their parameters. R-F, which is being deprecated, simply didn't keep up. – Scott Sauyet Jun 05 '17 at 13:57
  • As to `map`, I was simply suggesting that using the same interface for arrays as for all other functors was appropriate to something like Ramda. `R.map` passes only the value into the callback, whereas `[].map` passes several additional parameters, which can cause issues when they're not expected, e.g. ['1', '2', '3'].map(parseInt); //=> [1, NaN, NaN]` vs Ramda's R.map(parseInt, ['1', '2', '3']); //=> [1, 2, 3]` (because of the optional `radix parameter to `parseInt` and `[].map` passing the index.) – Scott Sauyet Jun 05 '17 at 14:02
  • Thanks for the follow-up comments, @ScottSauyet. – Dapeng Li Jun 05 '17 at 16:13
  • @ScottSauyet your example for `madd3` was perfect, I finally understand the example now. Thanks! – Kyle Pittman Nov 01 '18 at 16:32
  • @Monkpit: glad it helped. `lift` is really not that complicated a concept, but often it gets buried in terminology. – Scott Sauyet Nov 01 '18 at 17:20
8

lift/liftN "lifts" an ordinary function into an Applicative context.

// lift1 :: (a -> b) -> f a -> f b
// lift1 :: (a -> b) -> [a] -> [b]
function lift1(fn) {
    return function(a_x) {
        return R.ap([fn], a_x);
    }
}

Now the type of ap (f (a->b) -> f a -> f b) isn't easy to understand either, but the list example should be understandable.

The interesting thing here is that you pass in a list and get back a list, so you can repeatedly apply this as long as the function(s) in the first list have the correct type:

// lift2 :: (a -> b -> c) -> f a -> f b -> f c
// lift2 :: (a -> b -> c) -> [a] -> [b] -> [c]
function lift2(fn) {
    return function(a_x, a_y) {
        return R.ap(R.ap([fn], a_x), a_y);
    }
}

And lift3, which you implicitly used in your example, works the same - now with ap(ap(ap([fn], a_x), a_y), a_z).

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 3
    I find arity-specific lifting functions such as these much more intuitive than Ramda's variadic `lift`. Sanctuary provides [`lift`](http://sanctuary.js.org/#lift), [`lift2`](http://sanctuary.js.org/#lift2), and [`lift3`](http://sanctuary.js.org/#lift3). – davidchambers Apr 11 '16 at 23:30
  • Sorry Bergi, that comment was directed toward the OP. – Mulan Apr 12 '16 at 14:01
  • @naomik: I figured so :-) – Bergi Apr 12 '16 at 14:11
  • Thank you for the answer. I'm still chewing through it dissecting `ap` and understanding what Applicative context refers to (I'm assuming https://github.com/fantasyland/fantasy-land#applicative). – diplosaurus Apr 12 '16 at 20:11
  • 1
    @diplosaurus: Yes, that one. Also good reads on the subject: http://learnyouahaskell.com/functors-applicative-functors-and-monoids and http://stackoverflow.com/questions/6570779/why-should-i-use-applicative-functors-in-functional-programming. "An applicative context" in this case (specifically, Ramda) refers to the List/Array context (as the second line type signatures exemplify), but it could be another Applicate like Maybe. – Bergi Apr 12 '16 at 23:13