This is part 5 of a series on Implementing a (kinda) fantasy land compliant Maybe type. For part 1, go here, part 2 is here, part 3 can be found here and 4 here
Our Maybe is growing a lot. So far, we’ve implemented instances of:
- Semigroup (adds
concat
) - Monoid (adds
empty
on the type representative ) - Functor (adds
map
) - Apply (adds
ap
)
Next one on the list is Applicative. But, before we get there, there was one thing bugging me a lot as I got here, and that was that I had no proper way of checking my values for equality. Now, this is a problem for Javascript the language in itself (look here), but that can’t stop us! In fact, one of the algebras defined in the fantasy-land spec provides us with exactly what we want.
Enter the Setoid
Here’s what the spec has to say:
equals :: Setoid a => a ~> a -> Boolean
So, it’s a method called equals on an object with an instance of Setoid that receives one parameter and returns a boolean
. Sort of like this: setoid.equals(b)
and there’s just one rule: b
must be of the same Setoid instance. So, in our case, our Maybe has to be able to know if it’s equal to another Maybe. So, here are the test cases we need to cover in plain English:
- A Nothing is equal to another Maybe if the other Maybe is also a Nothing.
- A Nothing is not equal to another Maybe if the other Maybe is a Just.
- A Just is not equal to another Maybe if that Maybe is a Nothing.
- A Just is equal to another Maybe if that Maybe is a Just and the values inside each of them are also equal to one another.
And, here are our tests for that:
it("should return true for 2 Nothing", () => {
const actual = Nothing().equals(Nothing());
expect(actual).toBe(true);
});
it("should return false for 1 Nothing and 1 Just", () => {
const actual = Nothing().equals(Just(1));
expect(actual).toBe(false);
});
it("should return false for 1 Just and 1 Nothing", () => {
const actual = Just(1).equals(Nothing());
expect(actual).toBe(false);
});
it("should return true for 2 Just with the same value inside", () => {
const actual = Just(1).equals(Just(1));
expect(actual).toBe(true);
});
And, like we did in map
, it makes sense here to also defer the responsibility to a helper function that will work for any possible Setoid in the future. Here’s how that looks:
const sameType = (x, y) =>
typeof x === typeof y &&
typeof x.constructor !== "undefined" &&
typeof y.constructor !== "undefined" &&
x.constructor.typeRepresentation === y.constructor.typeRepresentation;
const fantasyEquals = (x, y) => {
if (!sameType(x, y)) return false;
if (
typeof x.constructor.typeRepresentation !== "undefined" &&
typeof y.constructor.typeRepresentation !== "undefined" &&
x.constructor.typeRepresentation === y.constructor.typeRepresentation
)
return fantasyEquals(x.value(), y.value());
return x === y;
};
const Nothing = value => ({
// …
equals: other => other.isNothing(),
// …
});
const Just = value => ({
// …
equals: other => (other.isNothing() ? false : fantasyEquals(Just(value), other)),
// …
});
We ask first if both arguments the function received are of the same type, following our type conventions. If they are, then we check if they are also algebras of the same type, and if so we re-iterate on the inner values. Our goal with this function is to eventually reach a primitive value inside of our Setoid that we can directly compare for equality, which is what happens on the last line with x === y
. Note: I’m aware this function fails for equal arrays and equal objects in JS, but I didn’t want to complicate it too much for the post. The final version in the GitHub repo does contain a naive equality check for these kind of structures.
There are also a couple laws to test in here:
it("should return true for 2 Nothing", () => {
const actual = Nothing().equals(Nothing());
expect(actual).toBe(true);
});
it("should return false for 1 Nothing and 1 Just", () => {
const actual = Nothing().equals(Just(1));
expect(actual).toBe(false);
});
it("should return false for 1 Just and 1 Nothing", () => {
const actual = Just(1).equals(Nothing());
expect(actual).toBe(false);
});
it("should return true for 2 Just with the same value inside", () => {
const actual = Just(1).equals(Just(1));
expect(actual).toBe(true);
});
And, like we did in map
, it makes sense here to also defer the responsibility to a helper function that will work for any possible Setoid in the future. Here’s how that looks:
const reflexivity = a => a.equals(a) === true;
const symmetry = (a, b) => a.equals(b) === b.equals(a);
it("should fulfil the law of reflexivity", () => {
expect(jsc.checkForall(maybeArb, reflexivity)).toBe(true);
});
it("should fulfil the law of symmetry", () => {
expect(jsc.checkForall(maybeArb, maybeArb, symmetry)).toBe(true);
});
That’s it for the detour, but the cool thing is that we can now more accurately test our implementations against equality.
Applicativeness
According to the spec, there’s just one small looking thing we should do here:
of :: Applicative f => a -> f a
>F.of(a)
With a couple rules:
- Any Applicative must also have an instance of Apply ✅
- This method is on the type representative, not on the type constructors. In our case, that means it’s on Maybe and not on our Just / Nothing functions.
- It should return a value of the same Applicative. So, our
Maybe.of
function, should take one argumenta
of any type, and return a Maybe. - No parts of
a
should be checked. This is the one that makes us think a bit.
When I saw this for the first time, my first assumption was to build a function that checked wether the value a
“existed” as in wasn’t undefined or null and return a Nothing if it did not exist, or a Just of it if it did exist. But then I read the last rule: No parts of a
should be checked. If that’s the case, then we have no way of knowing wether it makes sense to return a Nothing or a Just. That leaves us with just one option, which is always returning a Just of the value that gets passed along.
So, here’s how our function looks like on our Maybe type representative.
const Maybe = {
empty: () => Nothing(),
of: value => Just(value),
typeRepresentation: "Maybe",
};
It may look simple, but remember how my first reflex was to do something completely wrong. And, like with all of our algebras, there are some laws that need to be checked:
const applicativeIdentity = algebra => x => algebra.of(x).ap(algebra.of(id)).equals(algebra.of(x));
const applicativeHomomorphism = algebra => (f, x) =>
algebra
.of(x)
.ap(algebra.of(f))
.equals(algebra.of(f(x)));
const applicativeInterchange = algebra => (u, y) =>
algebra
.of(y)
.ap(u)
.equals(u.ap(algebra.of(f => f(y))));
it("should fulfil the identity property", () => {
expect(jsc.checkForall(maybeArb, applicativeIdentity(Maybe))).toBe(true);
});
it("should fulfil the homomorphism property", () => {
expect(jsc.checkForall(jsc.fn(jsc.string), jsc.string, applicativeHomomorphism(Maybe))).toBe(true);
});
it("should fulfil the interchange property", () => {
expect(jsc.checkForall(maybeFnArb, jsc.string, applicativeInterchange(Maybe))).toBe(true);
});
So far so good, right?
- Setoid ✅
- Semigroup ✅
- Monoid ✅
- Functor ✅
- Apply ✅
- Applicative ✅
So far we’ve implemented 6 instances, and now we have only 4 algebras left to go! Those are:
- Foldable coming up next!
- Traversable
- Chain
- Monad
This means, we’ll get a minimum of 3 more posts on this series, which started out as something quick to take care of in a couple hours and grew a bit out of proportion. But hasn’t stopped being fun.