A while back, I wrote a function that checked for nested properties inside arrays. In that post, I mentioned how this pattern would look better if I were to use a Maybe. Then, Wolfram challenged that assumption by asking to see a comparison of the 2 implementations, a challenge that prompted an 8 part journey through Fantasy-Land while we implemented our very own Maybe in Javascript.
And now, for the grand finale, I will show you the wonders that using algebraic abstractions can do for your code base. Or, you know, just the very tiny differences in the code in this case.
Algebra never tasted so good
Let’s go, function by function. Here’s what the first, non-maybe version looks like:
const NOTHING = {}; // this could be anything. undefined, null, you name it.
const safeProperty = (obj, propertyName) => (obj[propertyName] ? obj[propertyName] : NOTHING);
And, this is our new shiny Maybe version:
const maybeSafeProperty = (obj, propertyName) => (obj[propertyName] ? Just(obj[propertyName]) : Nothing());
It looks the same, right? We’re just adding a couple wrappers around our return values. Otherwise, it’s exactly the same. But, let’s look at the type signature for both functions now:
safeProperty :: o -> s -> ?¿?
>maybeSafeProperty :: Maybe m => o -> s -> m a
Ok, so what happened there? We ran into a problem while trying to give our safeProperty
function a type signature because we can’t be sure what it returns. It either returns anything which is contained in the property inside the object, or it returns an empty object. But, there’s no way we can be sure those are represented by the same type, meaning the result could be a string or an empty object. That’s not so pretty.
In comparison, our maybeSafeProperty
will always have the same type of return value, a Maybe a
where a
is anything (we don’t know what’s inside the object, so it needs to be anything). So there is a difference, and it’s a big one in this case. It’s giving us some modicum of type safety. Not a lot, mind you, since this is Javascript after all y’all.
On to the next function:
const recurProperties = (obj, ...propertyNames) =>
propertyNames.reduce(
(accValue, currentProperty) => (accValue[currentProperty] ? accValue[currentProperty] : NOTHING),
obj
);
And while implementing the Maybe one, I realized something: I needed to change the first function in order to get it to work. The reason for this is that we have a function with an arity of 2 (takes 2 arguments). But, we want to chain
a function inside of the reduce, the function that chain
gets is unary (only 1 argument). So, the following happened:
// Currying to the rescue! Also, order of parameters changed.
const maybeSafeProperty = propertyName => obj => (obj[propertyName] ? Just(obj[propertyName]) : Nothing());
const maybeRecurProperties = (obj, ...propertyNames) =>
propertyNames.reduce((accValue, currentProperty) => {
return accValue.chain(maybeSafeProperty(currentProperty));
}, Just(obj));
Now, this one is quite different. Let’s go over each part and analyse the differences:
const recurProperties = (obj, ...propertyNames) =>
// We reduce over the property names
propertyNames.reduce(
(accValue, currentProperty) =>
// If the current value has a property named as the current value in the
// propertyNames array, then we return that as the next accumulated value
// Otherwise, we return Nothing (which will keep being returned until the end, since it’ll never have a valid property).
accValue[currentProperty] ? accValue[currentProperty] : NOTHING,
obj // Initial value is our object argument.
);
const maybeRecurProperties = (obj, ...propertyNames) =>
// Same as above. Reducing over property names
propertyNames.reduce(
(accValue, currentProperty) => {
// We use the first function in our Maybe’s chain.
// chain is used here instead of map, because our maybeSafeProperty
// returns a Maybe. So, if we mapped, we’d have double the structure around.
// chain takes care of that for us.
return accValue.chain(maybeSafeProperty(currentProperty));
},
Just(obj) // initial value is our argument object wrapped in a Just.
);
The main difference here is that the second version gives us a straightforward way to re-use the function we had already written, without caring about checking for what the value is inside of this function. We don’t need that, because we know how our values will behave if they’re Just and if they’re Nothing. I think that’s quite a nice thing to have.
Next please:
const ifPropertyThen = (fn, obj, ...propertyNames) =>
recurProperties(obj, ...propertyNames) === NOTHING ? NOTHING : fn(obj);
const maybeIfPropertyThen = (fn, obj, ...propertyNames) =>
maybeRecurProperties(obj, ...propertyNames).isJust() ? Just(obj).map(fn) : Nothing();
Ugh. This pair doesn’t look too good. But the reason they both look pretty much the same is because we’re not really correctly using our recurProperties
function. Here we check if that value is not a Nothing in both cases, but we don’t actually use that value. Not much to see here, other than this is not the greatest function. So, I wrote another one:
const propertyThen = (fn, obj, ...propertyNames) => {
const property = recurProperties(obj, ...propertyNames);
return property === NOTHING ? NOTHING : fn(property);
};
const maybePropertyThen = (fn, obj, ...propertyNames) => maybeRecurProperties(obj, ...propertyNames).map(fn);
Shine Maybe, shine! In this one, we do use the value we get, and it helps to show what our algebraic structure is doing for us. In the first function, we first have to find the value. Then, if it’s Nothing, we return Nothing, otherwise we apply the function argument on the property we got. And there’s no way around this, since our Nothing and our property don’t have any built in mechanism to get functions applied to them. Not doing this check would blow things up for sure and is a recipe for disaster.
In comparison, our Maybe version is a beautiful one liner, because the value we get from maybeRecurProperties
is either a Just, which will then apply the function to its value, or a Nothing which will just ignore the function and return Nothing. Exactly the same behaviour. No value checks. I call that profit.
Trading things off
We got some changes, and some safety in exchange for adding a lot of boilerplate to our code base. Was it worth it you ask? Well… not really, at least not in this case. Unfortunately, using this kind of structures in this way is a hard opt-in, meaning that you either go all in and use them everywhere in your code base, or not at all. The idea behind them is inter-operatibility after all, and that means that we can use things without worrying if they’ll work. We’ll know they work, because we have that algebraic structure behind.
Still, I deeply enjoyed building that Maybe. It gave me a lot of insight into what the algebras actually mean, and all the amazing work that goes behind implementing a language like Haskell. And I do believe that this structures in Javascript can be valuable and useful for all. I just haven’t figured out where the intersection between adding the complexity and the value we get from it lies yet.