Skip to content

Instantly share code, notes, and snippets.

@junosuarez
Created April 16, 2013 08:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save junosuarez/5394373 to your computer and use it in GitHub Desktop.
Save junosuarez/5394373 to your computer and use it in GitHub Desktop.
Explanation of liftA2 in JavaScript via http://www.techtangents.com/explanation-of-lifta2-in-javascript/ with examples rewritten in jsig

This came out of the recent JS Promises debate. @ForbesLindesay was confused by liftA2, so I thought I’d try to explain it.

Let me explain liftA2. I’ll tweak @pufuwozu’s examples so that they return values.

// Promises (called when both succeed)
liftA2(readFIle('hello.txt'), readFile('world.txt'), function(hello, world) {
 return hello + ' ' + world;
});
// Optional values (only inserted into database when both exist)
liftA2(optionalUsername, optionalPassword, function(username, password) {
 return {username: username, password: password);
});
// Arrays (function called for each element of each array)liftA2([1, 2, 3], [4, 5, 6], function(a, b) { return a + b;});

In the first example, we have 3 arguments:

  • two Promise<String>s
  • a function that takes 2 Strings and builds them into another string

In this instance, liftA2 returns a Promise<String>. This Promise, when evaluated, evaluates the two input Promises and runs their resulting values through the input function.

In the second example, we have 3 arguments:

  • two Option<String>s
  • a function that takes 2 Strings and builds them into an Object.

In this instance, liftA2 returns an Option<Object>. If both input Options are “some”, it runs them through the function to generate “some” of the resulting. If either input is “none”, the function is not called and “none” is returned.

In the third example, we have 3 arguments:

  • two Array<Number>s
  • a function that takes 2 Numbers and adds them together to form another Number.

In this instance, liftA2 returns an Array<Number>. Each element in the resulting array is the sum of one Number from the first Array and one Number from the second Array. Each combination of an element in the first and the second is run through the function.

Let’s look at those types. (A) => B just means a function that takes an ‘A’ and returns a ‘B’.

  • (Promise<String>, Promise<String>, (String, String) => String)) => Promise<String>
  • (Option<String>, Option<String>, (String, String) => Object)) => Option<Object>
  • (Array<Number>, Array<Number>, (Number, Number) => Number)) => Array<Number>

Note that there is ONE implementation of liftA2, not one for each datatype. Just one. So, how can one function deal with data of vastly different type? It all comes down to a pattern – Applicatives.

Promise, Option and Array are all Applicatives (as are all Monads). liftA2 takes two arguments of the same type of Applicative, with the same parameter type (actually the universal type in javascript). It “extracts” the values from the Applicatives, runs them through the function, then “boxes them back up” into a value of the applicative type.

So, what does it mean to “extract” and “box back up”? Well, that depends on the Applicative! It’s what makes one Applicative different from another! That’s exactly why liftA2 can apply to such vastly different data structures – because the relevant differences have been encapsulated in their implementation of Applicative.

So, what’s the type of this liftA2 function in JavaScript?

for all Applicatives F. for all types A, B, C. (F<A>, F<B>, (A, B) => C) => F<C>

If you look at the types above, you’ll notice they all match this pattern, hence they all meet the requirements to call liftA2.

So, what does it require to be an Applicative?

You need to be a Functor, so you need a ‘map’ function. You need a “point” function that wraps a value in the applicative. You need an “ap” function of this type: for your Applicative F. for all types A, B, C. (F<A>, F<B>, F<(A, B) => C>) => F<C> And, what does liftA2 look like? Courtesy of @pufuwozu

function liftA2(promiseA, promiseB, fn) {
  return ap(promiseB, map(promiseA, function(a) {
    return function(b) {
      return fn(a, b);
    };
  }));
}

That’s it? Really? That’s all it takes? For real. So much power, so little code. That’s the power of abstractions.

@CrossEye
Copy link

Missing a line-break in the initial example.

@dotnetCarpenter
Copy link

@jden what does ap look like?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment