This is part 4 of a series on Implementing a (kinda) fantasy land compliant Maybe type. For part 1, go here, part 2 is here and part 3 can be found here

Last time, we ended up with a Functor instance on our Maybe! This time, we’ll go right ahead and add a Apply instance.

Ap Ap

If we look at the spec, we get the following:

An instance of Apply must implement an ap method: ap :: Apply f => f a ~> f (a -> b) -> f b

So, if we have an Apply a, then we can do a.ap(b) to use the ap function. And there are a couple of rules:

  • b must be an Apply of a function, and the same type of Apply as a.
  • ap must apply the function in Apply b to the value in Apply a.

Wait, what? Right. Same reaction here at first, so let’s try to unwrap the type definition first:

  • ap :: Apply f => ➡ name of the function and a type restriction. This means that every time we see an f in the signature, we know that f must be an Apply.
  • f a ~> ➡ Remember the squiggly line? It means that this function (ap) is a method of an Apply of a.
  • f (a -> b) -> ➡ This is the argument that ap gets. And it means what is stated in rule 1, the argument must be an Apply of a function (that’s what (a -> b) represents, a function that takes an a and returns a b.
  • f b ➡ Return type of the function. An Apply of the result of applying the function that came before (a -> b).

If it still looks a bit fuzzy, do not panic! This one’s weird, specially for those of us who don’t come from a functional background. Let’s keep moving and hope implementing the function clears it up a bit.

Applying on

While implementing our Functor instance, we decided that Nothing.map = Nothing. So, it makes sense that thats the same behaviour we provide for ap.

Here are some tests and the implementation that makes them pass:

// Nothing.ap === Nothing
it("should always return Nothing when called on a Nothing", () => {
const actual = Nothing()
.ap(Just(x => x))
.equals(Nothing());
expect(actual).toBe(true);
});

//Just.ap(Nothing) === Nothing
it("should always return Nothing when called on a Just with a Nothing as argument", () => {
const actual = Just(9).ap(Nothing()).equals(Nothing());
expect(actual).toBe(true);
});

const Nothing = value => ({
value: () => "Nothing",
isNothing: () => true,
isJust: () => false,
equals: other => other.isNothing(),
concat: other => other,
map: fn => Nothing(),
ap: other => Nothing(),
constructor: Maybe,
toString: () => "Nothing",
inspect: () => "Nothing",
instances: ["Semigroup", "Monoid", "Functor", "Apply", "Applicative"],
});

const Just = value => ({
value: () => value,
isNothing: () => false,
isJust: () => true,
equals: other => (other.isNothing() ? false : fantasyEquals(Just(value), other)),
concat: other => (other.isNothing() ? Just(value) : Just(fantasyConcat(value, other.value()))),
map: fn => Just(fantasyMap(fn, value)),
ap: other => {
if (!other.isJust()) return other;
},
constructor: Maybe,
toString: () => `Just(${value})`,
inspect: () => `Just(${value})`,
instances: ["Semigroup", "Monoid", "Functor", "Apply", "Applicative"],
});

Note: if you’re wondering where the heck that .equals came from, I’ll explain later! Really, I will! For now, you’ll just have to trust me when I say it was necessary and I was tired of all the toString hacks.

Now, for the second part in which we have a Just and the value inside of ap is another Just.

it("should apply the function inside of the argument to the value of the Just being called on, and return a Just of the result", () => {
const expected = Just(3).value();
const actual = Just(9).ap(Just(Math.sqrt)).value();
expect(actual).toBe(expected);
});

const Just = value => ({
// …
ap: other => (other.isJust() ? Just(fantasyMap(other.value(), value)) : other),
// …
});

What happened here? Let’s go step by step:

  • other.isJust() ? ➡ we inverted the conditional to ask first if the argument we got is a Just instead of the opposite. And we’ll be using the ternary operator in the function.
  • Just(fantasyMap(other.value(), value)) ➡ first part of the ternary. If the condition is true, then this gets returned. And what’s happening inside, is basically the same as map, only we replace the function with our argument’s value (remember, the argument must be an Apply of a function) . We also re-wrap it in Just like we did in map because our function tells us the result must be of the same Apply type as what it was called on.
  • : other ➡ if our argument is not a Just, then it’s a Nothing. And if it’s a Nothing, we return Nothing, which is the same as returning the argument itself.

And that’s mostly it. Not too shabby if I may say so myself.

Ain’t no Algebra without properties

Of course there are properties that we must fulfil. Here’s the one for Apply:

v.ap(u.ap(a.map(f => g => x => f(g(x))))) is equivalent to v.ap(u).ap(a) (composition)

This one’s… verbose. But what it’s basically saying is that applying twice is the same as applying once with the composition of the 2.

And here’s how we write the property and the test for it:

const applyComposition = (a, u, v) => v.ap(u.ap(a.map(f => g => x => f(g(x))))).equals(v.ap(u).ap(a));

const maybeFnArb = jsc.fn(jsc.string).smap(
Just,
x => x.value(),
y => y.toString()
);

it("should fulfil the composition property", () => {
expect(jsc.checkForall(maybeFnArb, maybeFnArb, maybeArb, applyComposition)).toBe(true);
});

maybeFnArb is an arbitrary function that must return an arbitrary Just(string).

That’s it, done, finito! Now, you may be wondering:

Why in heaven’s name

I know, wasn’t map enough? Why do we want this instance, why would we need to have our function wrapped inside of the Maybe? Well, I’ll be the first to admit, you might never need this, especially in normal day to day non FP Javascript. I know I haven’t yet. But the theory behind it is quite sound, hear me out:

Sometimes, you might want to apply a function you don’t know exists. So, the value of your function could either be wrapped inside a Just or be a Nothing. And you don’t want your code to blow up, which is exactly what it would do if you used map in such a situation. So, ap and the Apply algebra are intended for those kinds of situations, where you don’t know wether that function is there or not. It’s all about feeling more confident in your code’s ability to not blow up, or at least that’s how I see it.

So:

  • Semigroup ✅
  • Monoid ✅
  • Functor ✅
  • Apply ✅
  • Applicative up next! (With a little Setoid detour).