Can you give me an example of how to use Ramda lift?

Functional programming is a long and mathematical topic, in particular the part dealing with monads and cathegory theory in general. But it is worth to take a look at it, here is a funny introduction with pictures.

In short, lift is a function that will take a n-arguments function and will produce a function that takes n wrapped-values and produces another resulting wrapped-value. A lift that take a one-argument function is defined by the following type signature

 // name :: f is a wrp-value => function -> wrp-value -> wrp-value
 liftA :: Applicative f   => (a -> b) -> f a -> f b

Wait... Wrapped-value?

I will introduce briefly Haskell, only to explain this. In haskell, an easy example of wrapped-value is Maybe, Maybe can be a wrapped-value or nothing, that is also a wrapped-value. The following example applies a function to a Maybe containing a value, and a empty Maybe.

> liftA (+ 8) (Just 8)
Just 16
> liftA (+ 8) Nothing
Nothing

The list is also a wrapped-value, and we can apply functions to it. In the second case liftA2 applies two-argument functions to two lists.

> liftA (+ 8) [1,2,3]
[9,10,11]
> liftA2 (*) [1..3] [1..3]
[1,2,3,2,4,6,3,6,9]

This wrapped-value is an Applicative Functor, so from now I will call it Applicative.

Maybe Maybe you are starting to lose interest from this point... But someone before us has got lost on this topic, finally he survived and published it as an answer to this question.

Lets look at what did he see...

...

What did you see? (xkcd comic)

He saw Fantasy Land

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

  • Fantasy-land is a fancy name to a functional programming spec in javascript. Ramda follows it.
  • Apply is our Applicative, a Functor that implements also an ap method.
  • A Functor, is something that has the map method.

So, wait... the Array in javascript has a map...

[1,2,3].map((a)=>a+1) \\=> [ 2, 3, 4 ]

Then the Array is a Functor, and map applies a function to all values of it, returning another Functor with the same number of values.

But what does the ap do?

ap applies a list of functions to a list of values.

Dispatches to the ap method of the second argument, if present. Also treats curried functions as applicatives.

Let's try to do something with it.

const res = R.ap(
  [
    (a)=>(-1*a), 
    (a)=>((a>1?'greater than 1':'a low value'))
  ], 
  [1,2,3]); //=>  [ -1, -2, -3, "a low value", "greater than 1", "greater than 1" ]

console.log(res);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ramda.min.js"></script>

The ap method takes an Array (or some other Applicative) of functions an applies it to a Applicative of values to produce another Applicative flattened.

The signature of the method explains this

[a → b] → [a] → [b]
Apply f => f (a → b) → f a → f b

Finally, what does lift do?

Lift takes a function with n arguments, and produces another function that takes n Aplicatives and produces a flattened Aplicative of the results.

In this case our Applicative is the Array.

const add2 = (a, b) => a + b;
const madd2 = R.lift(add2);

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

console.log(res);
// Equivalent to lift using ap
const result2 = R.ap(R.ap(
  [R.curry(add2)], [1, 2, 3]),
  [2, 3, 4]
  );
//=> [3, 4, 5, 4, 5, 6, 5, 6, 7]
console.log(result2);
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ramda.min.js"></script>

These wrappers (Applicatives, Functors, Monads) are interesting because they can be anything that implements these methods. In haskell, this is used to wrap unsafe operations, such as input/output. It can also be an error wrapper or a tree, even any data structure.


This function can only accept numbers:

const add3 = (a, b, c) => a + b + c;
add3(1, 2, 3); //=> 6

However what if these numbers were each contained in a functor? (i.e. a thing that contains a value; an array in the example below)

add3([1], [2], [3]); //=> "123"

That's obviously not what we want. You can "lift" the function so that it can "extract" the value of each parameter/functor:

const add3Lifted = lift(add3);
add3Lifted([1], [2], [3]); //=> [6]

Arrays can obviously hold more than one value and combined with a lifted function that knows how to extract the values of each functor, you can now do this:

add3Lifted([1, 10], [2, 20], [3, 30]);
//=> [6, 33, 24, 51, 15, 42, 33, 60]

Which is basically what you'd have got if you had done this:

[
  add3(1, 2, 3),    // 6
  add3(1, 2, 30),   // 33
  add3(1, 20, 3),   // 24
  add3(1, 20, 30),  // 51
  add3(10, 2, 3),   // 15
  add3(10, 2, 30),  // 42
  add3(10, 20, 3),  // 33
  add3(10, 20, 30)  // 60
]

Note that each array doesn't have to be of the same length:

add3Lifted([1, 10], [2], [3]);
//=> [6, 15]

So to answer your question: if you intend to run a function with different sets of values, lifting that function may be a useful thing to consider:

const results = [add3(1, 2, 3), add3(10, 2, 3)];

is the same as:

const results = add3Lifted([1, 10], [2], [3]);

What hasn't been mentioned in the current answers is that functions like R.lift will not only work with arrays but any well behaved Apply1 data type.

For example, we can reuse the same function produced by R.lift:

const lifted = lift((a, b, c) => a + b - c)

With functions as the Apply type:

lifted(a => a * a,
       b => b + 5,
       c => c * 3)(4) //=> 13

Optional types (dispatching to .ap):

const Just = val => ({
  map: f    => Just(f(val)),
  ap: other => other.map(otherVal => val(otherVal)),
  getOr: _  => val
})

const Nothing = {
  map: f    => Nothing,
  ap: other => Nothing,
  getOr: x  => x
}

lifted(Just(4), Just(6), Just(8)).getOr(NaN) //=> 2

lifted(Just(4), Nothing, Just(8)).getOr(NaN) //=> NaN

Asynchronous types (dispatching to .ap):

const Asynchronous = fn => ({
  run: fn,
  map: f    => Asynchronous(g => fn(a => g(f(a)))),
  ap: other => Asynchronous(fb => fn(f => other.run(a => fb(f(a)))))
})

const delay = (n, x) => Asynchronous(then => void(setTimeout(then, n, x)))

lifted(delay(2000, 4), delay(1000, 6), delay(500, 8)).run(console.log)

... and many more. The point here is that anything that can uphold the interface and laws expected of any Apply type can make use of generic functions such as R.lift.

1. The argument order of ap as listed in the fantasy-land spec is reversed from the order supported by name dispatching in Ramda, though is still supported when using the fantasy-land/ap namespaced method.