Functional Types in JS: Writing a Writer
When I was first trying to teach myself about functional programming, one of the trickiest bits to wrap my head around was what a whole application built with just this mad, wonderful stuff would actually look like.
No matter how many amazingly eloquent examples of the functional style you look at, there’s a big difference between knowing how to turn a phrase now and then and knowing how to plan out and write a well-plotted novel. Worse, using the functional style very forcibly (and very deliberately!) rules out a lot of the sloppy anti-patterns we’d normally rely on everyday to get the job done… leaving us without any idea how to tackle those larger structural concerns.
So this time around we’ll be looking into some of the high-order Types that can help bottle the mess of boilerplate and imperative bumbling. While we’ll eventually get into deeper topics like an actual State Type (ooo, auspicious!) we’ll start with a much simpler construct that we’ll dub Writer (a name taken from a similar type in Haskell and also adapted from this great Monad intro from James Colgan. First off, lets look at the problem space that Writer targets.
It’s super easy to fudge that restriction of course: that one “thing” could we return could be an array or an object containing any sort of data or structure we please. I’m not saying you couldn’t make it work:
But at least when it comes to programs, freedom isn’t always a good thing. Once we start writing functions that return bespoke datatypes of any shape or size, we’re forced into writing overly specific or complex downstream functions that have to consume and interpret this special output. And that quickly gets confusing, inflexible, and WETT (Write Everything Twenty Times).
There are a bunch of different ways to tackle this problem of multiple values. One that many people end up settling on for larger application design is simply… closure: doing things in the context of an outer function/scope that can remember extra bits and pieces of information as an internal computation runs its course.
Plus, there are a lot of bad ways to tackle this problem of multiple values. For instance, we could easily end up splitting up our work in the wrong ways: like breaking down “anotherComplexFunction” from the example above and cramming whatever it wanted to do with a and b back up into the “complexFunction” block:
Take a deep breath… ok… there are functional solutions. And today, let’s look at the simplest imaginable: something we said we’d call Writer.
Writer is actually going to be an overly specific implementation of a Tuple type. What that means is that it is a sort of container (like an Array) that always represents two related pieces of data at a time instead of just one. Here’s the constructor, loosely modeled after the “tagged constructor” approach used in daggy:
You can see that we intend to treat the first argument (left-side) as a “log” value, and the second (right-side) argument, “value,” as something that can hold any value at all (including arrays, objects, functions, other higher-order types, etc).
With all this in place, we can now call Writer(‘a five’, 5) And what we get back is a sort of “annotated” value type: Writer[‘a five’, 5]. If we want, we can get the value back out by calling .snd() and the annotation back out by calling .fst()
This really isn’t very useful by itself: all we can do is create Writers and then extract their values again. But we can make it useful by turning Writer into a Functor:
Ah! Now we have a way to pass functions into a Writer, each of which will modify just the second value and then give us back a new Writer with the original log statement but an updated value. Note that the functions we’re applying to the value in our Writer are just regular, uncomplicated unary functions: map essentially “lifts” these mundane functions up into the more complex Writer type.
This is ultimately what will allow us to keep all those operations simple, DRY, and perhaps most importantly: completely ignorant of what a “Writer” is. Yep: that last bit is going to essentially solve our earlier problem of needing to capture and handle the extra complexity in all downstream functions we intend to write!
Let’s expand Writer’s capabilities a bit more. Since Writer is now a Functor, we might as well give it a way to handle the case where the “value” inside it is actually a function itself. That is, we can make it an Applicative (an interface by which a Writer containing a function can have the value inside another Writer applied to it):
Writer.of here just allows us to take any simple value and put it inside a Writer (complete with a default “log” value). And when one of those values happens to be a function, we now have a way to run it: using the value found inside another Writer.
We actually did something else new and interesting here: since we have two Writers at our disposal in this operation, we also have two left-side log values available to play with. You’ll note that we originally ensured that those left-side values are Strings (if this were a more general Tuple type, we probably wouldn’t make any such restriction). That means that left side values will always have a .concat() method available to them, which in functional land means that they are part of the same “Semigroup.” And that means that they “know” how to combine and “build up” a single result. So that’s exactly what we’ll do.
The end result is sort of neat: it’s one thing to have a 6 sitting around somewhere, but now we have a Writer type that can tell us the whole (admittedly very short), historical life story of that 6. That’s pretty cool, though let’s not pretend that we’re going to end up with some generic system which can deliver perfect English grammar in all cases:
Yeah, so it’s not Shakespeare… but that wasn’t really the point. What we originally wanted here was just to get a sense of how to mix normal functions with complex types and maybe accumulate a sort of running state alongside the normal computation. Writer, by virtue of its simplicity, makes that pattern easier to follow.
Writers and their various interfaces also have the virtue of being pure (every chain of writer operations will end up at the same place, given the same inputs) and immutable (each method just produces a new Writer instead of mutating the original).
Furthermore, because our Writer type conforms to the algebraic type specification outlined in fantasy-land, that means it’s much easier to create generic operations. Here’s a way to apply two Writer containers to a binary, curried function, for instance:
That’s great, but what’s even greater is that the same operation will work for any type of well-behaved Type that we might want to use. We can handle Arrays/Lists, Maybes, and Promises all the same, taking any generic function and just lifting it into a generic Type operation:
We’re not done: let’s try to make Writer even more powerful. We saw that a map operations don’t actually add anything to the Writer log, and that makes sense: it takes a simple function that returns a simple value: there’s no place for new “log” information to come into play. That was why we built Writer in the first place: to overcome this very problem!
(We could actually overload map if we wanted to so that it automatically used function.name of function.toString for the log… but hey: let’s not)
But we can still serve this particular need in a structured way: let’s try (…er) to make our Writer into a Monad! Which is to say, let’s give Writer a way to handle functions that intend to return new Writers, explicitly.
Writer’s .chain is, as hinted at, a sort of upgraded Writer.map: we can still pass in a function and that function will still receive the value inside the Writer as its input. But now we can return a new, custom-made Writer as a result, both transforming the value but also appending to the “log”… all in one go.
We can choose to add to the log or not with each operation we run, and the difference is simply the choice of chain vs map.
If we wanted to make that even easier, we could create a helper function to “writerize” an existing generic function, which is to say, take a generic unary function and easily upgrade it to something we can then pass to chain:
Well, neat in a specific-functionality sense, but not entirely lawful, I think. Test our version Writer out a bit and you’ll find that it fails those pesky Monadic Laws. That’s not the end of the world, but it probably means that our “.chain” interface was hastily named and should be probably be something else to avoid confusion and collision.
Could a different version of Writer be created to succeed where ours failed?
Yes. In fact, the problem with our Writer at the moment is that it tried to be too smart right at the start, with our implementation Writer.of. We defined that as being: x=>Writer(x,x) so that we’d start with a log value. But look what happens if that’s the case:
The pointed interface, of, unit, return, whatever you want to call it: its purpose is to create some type with the bare minimum of input it needs to exist. In the case of Writer, that bare minimum is a value paired with the emptiest implementation of the other implied value. Anything else assumes too much.
If we rewrite Writer.of, we can fix this problem:
At issue here is consistency: we expect to be able to freely juggle our operations around in lots of different ways, whatever best expresses a given problem space, while being assured that they’ll behave. But when Writer.of(x).chain(f) doesn’t behave like f(x) then suddenly that sense of freedom is replaced by anxiety and caution. Worse, if a Type implements a common algebraic interface but acts unpredictably out-of-step with other Types that implement it, our hope of higher abstractions will be dashed.
So while we were able to rescue ourselves this time, but there’s another important lesson here: it’s not always possible for a particular Type to represent every conceivable interface out there, and if we can’t make it lawful, we shouldn’t pretend to offer it in the first place. Not every type can implement a Monad interface, and some possible type implementations are just plain incompatible with it.
I talked about Const a while back, and I’m pretty sure it’s literally impossible to give it a valid monadic interface. What would you expect Const.of(9).chain(x=>Const.of(0+x)) to actually do, for instance? The whole point of Const was that its Functor interface ignores the function passed in and just returns the same “constant” value back in the same Const type. But if we implement .chain to do the same, it can’t pass the Monadic laws, and if we make it work like Identity… well, then it’s just Identity with the wrong name, not Const.
(When we talk about .sequence() down the road, we’ll see a similar impossibility with Promises/Tasks. Promise.of([6,7]).sequence(Array.of) has no viable implementation I can think of, at least.)
The Semigroup interface is a way of joining the inner logs/values of two Writers together, provided that those values both belong to the same “joinable” group. Common examples of value in such groups are Strings and Arrays: they both have .concat() methods. The left-side log value, guaranteed to be a String, always qualifies. The right-side value… well, we’ll just need to be careful:
Finally, making a Writer a Setoid just means giving us a way to compare two Writers for equality. All that means is checking to see if both their inner values match. Pretty simple:
The point of adding all these interfaces is to allow us to run all the sorts of functional operations we’re used to… but on values that are stored inside different higher-order Types (each of which model and control a specific context a given value might exist inside). As we saw with liftA2, if we take care to make sure those interfaces work the same ways across all the Types in our toolkits, we can write generic code that expresses what we want to have happen in a way that’s cleanly separated out from the more complex pattern that each type can express.
After all this, it’s probably time to come clean and admit that, hey, Writer is… probably not a particularly important type in and of itself. Its virtue was that it was simple: a sort of “Identity” implementation of multi-value types that helped us see how they could work. It’s certainly cool and all, but fairly limited in practical usage (an interesting way to log, or build up a string, I suppose). A real Tuple type, which isn’t restricted to just strings on the left-side could potentially be a much more powerful and flexible way to shuttle multiple values around.
But now that we’ve looked over the basics, next time can we start to tackle Reader, State, and maybe get a gander at what some fully-operational functional applications might look like. Thanks for reading!