What are these applicative functors you speak of?

What are they indeed. Our goal for today will be to learn about applicative functors using javascript. Yes, javascript. Don't judge me, it's what I know. We'll cover things like how to create them, how you can spot them in the wild and a somewhat useful use case.

Okay, let's start from the beginning.

What is a functor?

From a "technical" point of view you can think of them as containers of some sort. You see, the simplest way to implement a functor is by wrapping a value inside a data structure, then provide a method to interact with that value. This method is usually called map, its only purpose is to give us access to the value so we can transform it and then put the result back into the wrapper structure.

Let's see map in action. To make this less scary we'll look at a data type that we use all the time in javascript, arrays.

const numbers = [1];
const plus_one = (number) => number + 1;

numbers.map(plus_one);
// [ 2 ]

What happens here?

We have a number wrapped in an array, we use map to gain access to it and transform it using a callback function, and then the new value of the number gets wrapped in another array. That's it. That's basically the behaviour you want in a functor.

Now, arrays are not the only ones that have this behaviour, there is another data type that acts like this, Promise. In a Promise we don't have a map but we have a then which is close enough.

const number = Promise.resolve(1);
const plus_one = (number) => number + 1;

number.then(plus_one);
// Promise { <state>: "pending" }
// 2

Same thing happens here, we have a value in a structure (here a Promise), a method gives us access through a callback (that's then) and the new value gets wrapped in another instance of the same structure.

And that's the pattern. We covered what we needed to know about functors for now. If you want to know more about them check out this article: The Power of Map.

Ready to move on?

Applicatives

Applicatives are just functors with extra features. They give you the ability to merge two functors together. Specifically, they allow you to apply a function inside a functor to a value that's also inside a functor.

Wait... What? A functor that has function inside?

Yes. Putting a function inside a functor, like doing this.

const plus_one = (number) => number + 1;

// And then you put it in a box

[plus_one];

// Or

Promise.resolve(plus_one);

Why would someone do that?

Good question. The answer is, you wouldn't. I mean in the context of javascript is not a common thing to do. Doesn't mean applicatives are useless to us.

Back to our definition. Normally if you have a function and a value you would be able to apply the function using this syntax: some_function(some_value). That doesn't work if both are inside another structure. To "fix" this, applicatives have a method called ap (short for apply) which takes care of unwrapping each functor and applying the function to the value.

At this point I would love to show an example of a built-in data type that follows the rules of applicatives but I don't know of any. But do not fear, let's take this as an opportunity to do something else.

Building an Applicative from scratch

In order to keep this simple we are just going to make a thin wrapper around the Promise class. We are going to make Promise feel more functor-y and applicative-ish. Where do we start?

We want to make a "lazy promise". Usually a Promise executes the "task" we give it immediately but we don't want that now, this time we want to control when the task gets called. To achieve our goal we are going to create a method called fork, this will be the one that actually builds the Promise and sets the callbacks for success and failure.

function Task(proc) {
  return {
    fork(err, success) {
      const promise = new Promise(proc);
      return promise.then(success).catch(err);
    }
  }
}

Awesome. Now let's compare this we a normal Promise.

let number = 0;
const procedure = function(resolve, reject) {
  const look_ma = () => {
    console.log(`IT WORKED ${++number} times`);
    resolve();
  };

  setTimeout(look_ma, 1000);
};

new Promise(procedure); // This one is already running

Task(procedure); // This one doesn't do anything
Task(procedure)  // This does
  .fork(
    () => console.error('AAHHH!'),
    () => console.log('AWW')
  );

If you run that you should get these messages after 1 second.

IT WORKED 1 times
IT WORKED 2 times
AWW

Now that we have what we want, let's go to the next step.

As you know applicatives are functors, it means that now we need a map.

Let's go over one more time. What is the expected behaviour of map?

  1. It should give us access to the inner value through a callback function.
  2. It should return a new container of the same type. In our case it should return another Task.
  function Task(proc) {
    return {
+     map(fn) {
+       return Task(function(resolve, reject) {
+         const promise = new Promise(proc);
+         promise.then(fn).then(resolve).catch(reject);
+       });
+     },
      fork(err, success) {
        const promise = new Promise(proc);
        return promise.then(success).catch(err);
      }
    }
  }

What happens there? Well, first we receive an fn argument that's our callback. Then, we return a new Task. Inside that new Task we build the promise, just like in fork but this time it's "safer" because it doesn't run immediately. After that we just chain functions to the promise in their respective order, first the fn callback to transform the value, then the resolve function that will "end" the current task and finally the catch gets the reject function from the current task.

We can test this now.

const exclaim = (str) => str + '!!';
const ohh = (value) => (console.log('OOHH'), value);

Task((resolve) => resolve('hello'))
  .map(exclaim)
  .map(ohh)
  .fork(console.error, console.log);

If you run it as is you should get this.

OOHH
hello!!

But if you remove the fork you should get this.

Yes, a whole lot of nothing. Now we are done with the functory stuff.

We are half way there now. We have our functor pattern going on, now we need to make ap happen.

The way I see it ap is just like map but with a plot twist: the function we want to apply it's trapped inside another Task [dramatic music plays in the background].

With that idea in our minds we can write ap.

  function Task(proc) {
    return {
      map(fn) {
        return Task(function(resolve, reject) {
          const promise = new Promise(proc);
          promise.then(fn).then(resolve).catch(reject);
        });
      },
+     ap(Fn) {
+       return Task(function(resolve, reject) {
+         const promise = new Promise(proc);
+         const success = fn => promise.then(fn);
+         Fn.fork(reject, success).then(resolve);
+       });
+     },
      fork(err, success) {
        const promise = new Promise(proc);
        return promise.then(success).catch(err);
      }
    }
  }

Spot the difference? Don't worry I'll tell you anyway, the difference is that in order to get the callback function we use the fork of Fn instead of a raw Promise. That's it. Now see if it works.

const to_uppercase = (str) => str.toUpperCase();
const exclaim = (str) => str + '!!';

const Uppercase = Task((resolve) => resolve(to_uppercase));
const Exclaim = Task((resolve) => resolve(exclaim));
const Hello = Task((resolve) => resolve('hello'));

Hello.ap(Uppercase).ap(Exclaim)
  .fork(console.error, console.log);

We made it! Now we can merge values and functions inside applicatives! But we can't enter the applicative functors club just yet, we still need something more.

Applicatives must be able to put any value into the most simple unit of your structure.

The Promise class actually has something like that. Instead of doing this.

new Promise((resolve) => resolve('hello'));

We usually do this.

Promise.resolve('hello');

And after we use Promise.resolve we can immediately start calling methods like then and catch. That's what our Task is missing.

For this new "feature", we will need a static method. This one has different names in the wild, some call it "pure" others call it "unit" and the lazy ones call it "of".

Task.of = function(value) {
  return Task((resolve) => resolve(value));
};

We can finally say we have an applicative functor.

Something you can use in your day to day coding

Been able to create your own data type is nice, but wouldn't it be better if could just apply these patterns to existing types?

I have a good news and bad news. The good news is that we totally can. The bad news is that it will be a bit awkward.

Let's keep going with the Task theme we got going on. Let's say that we want to use map and ap with a Promise but we don't want create a new data type. What do we do? Some good old functions will do.

If you know the patterns and behaviours you should be looking for, writing some static functions in an object will be enough. This what our Task would look like as static functions (minus the "lazy" part).

const Task = {
  of(value) {
    return Promise.resolve(value);
  },
  map(fn, data) {
    return data.then(fn);
  },
  ap(Fn, data) {
    return Fn.then(fn => data.then(value => fn(value)));
  }
};

If you want to map you'll do something like this.

const to_uppercase = (str) => str.toUpperCase();

Task.map(to_uppercase, Task.of('hello'))
  .then(console.log);

ap also works in the same way.

const exclaim = (str) => str + '!!';

Task.ap(Task.of(exclaim), Task.of('hello'))
  .then(console.log);

I can feel your scepticism from here. Be patient, this will be good. Now, map looks kinda useful but ap not so much, right? Don't worry, we can still use ap for a greater good. What if I told you we can have like an "enhanced" version of map? Our map just works with functions that receive one argument and that's good but sometimes we need more.

Say that we have a function that needs two arguments but every time we use it those arguments come from two different promises. In our imaginary situation we have these functions.

function get_username() {
  return new Promise((resolve) => {
    const fetch_data = () => resolve('john doe'); 
    setTimeout(fetch_data, 1000);
  });
}

function get_location() {
  return new Promise((resolve) => {
    const fetch_data = () => resolve('some place'); 
    setTimeout(fetch_data, 500);
  });
}

function format_message(name, place) {
  return `name: ${name} | place: ${place}`;
}

When we use format_message its arguments almost every time come from those other functions get_username and get_location. They are asynchronous, so you might be tempted to use Async/await but that wouldn't be the best idea. Those two don't depend on each other, we will be wasting time if we make them run sequentially when they could be running concurrently. One solution can be found in the form of Promise.all, and it looks like this.

Promise.all([get_username(), get_location()])
  .then(([name, place]) => format_message(name, place))
  .then(console.log);

There you go. That works. But we can do better because we have applicatives on our side. Besides, we already wrote that Task object with all those functions. Let's add one more static function to Task that does the same thing Promise.all is doing for us here.

Task.liftA2 = function(fn, A1, A2) {
  const curried = a => b => fn(a, b);
  return Task.ap(Task.map(curried, A1), A2);
};

I'll explain the name later. Now let's see it action.

Task.liftA2(format_message, get_username(), get_location())
  .then(console.log);

Isn't this just slightly better?

And yes, several arguments could be made against this particular implementation of liftA2 and the Task itself, but all the patterns I've shown would work just fine with most of the applicative you can find in the wild.

As a fun exercise you can try to implement map and ap for Sets. See what kind of funny things you discover in the process.

Anyway, about that name liftA2. In functional programming when you take a function and make it work with container types like functors it is said that you're "lifting" the function to the "context" of that container. What do I mean by context? Okay, in the world of arrays when you use Array.map your function gets applied multiple times, in the context of a Promise your function runs only when the Promise is resolved. See what I mean? Good. The A2 part? Well, you know, it only works with binary functions so... that's why.

There is still one more trick you can do with applicatives but I still don't fully understand how it works, so maybe next time I'll show you that.

Conclusion

What did we learn today, class?

Y'all learned all that? My goodness. You're the best.

Okay, I guess my job here is done.

Sources


Have any question? Feel free to leave a comment in one of these platform where I have shared this:

You can reach out to me on social media:

Thank you for reading. If you find this article useful and want to support my efforts, buy me a coffee ☕

Buy Me A Coffee