Functors
A functor is a stand-in for a runtime value. In the same way that a variable’s symbol myNum
is distinct from its runtime value, (say, 5
) a functor myNumFn
is distinct from its runtime value. In the same way that I can write code using a symbol in Javascript to increment a numerical value at runtime,
const nextNum = myNum + 1
and I can write the same code using a functor in Javascript.
const nextNumFn = myNumFn.map(n => n+1)
This is more code, as Javascript lacks native syntax for functors, but the two statements serve the same purpose: creating a new value given an existing value and a computation. The advantage with a functor is that it further abstracts you from the runtime complexity of values. Unlike with a symbol, the functor can represent a nullable value, a collection of values, or a promise, all without changing the code to compute new values from old. This abstraction is driven by the utility of the map
function, which forces you to package your computation as a function, but allows the functor to execute that computation in whichever way it chooses.
Map has a lot of moving parts, but if you’ve ever used a Javascript array, you already know how it works. Map starts with an existing functor, which I’ll refer to as the “starter.” You then call map with some computation—a function which accepts the value of the starter as an argument and returns a new value. Functors are functional, so the computation doesn’t alter the starter’s values in place. Instead, map returns a new functor, which represents the values of our starter once the computation is applied to them.
Functors have a value type. A functor might only ever contain a string at runtime. It might represent many strings, or no strings, but the values are always strings. Functors themselves have a type, which isn’t to be confused with the type of values in represents. The type of functor dictates how values are stored and how computations are applied. A maybe functor represents a value that might or might not be there. If I have a maybe functor representing a string value and I want to check the strings length, I would use
const myLength =
myString.map(s => s.length)
Now I have a new maybe functor, myLength
. If myString
has a string at runtime, myLength
will get a length at runtime. If myString
does not have a string at runtime, myLength
will not get a length at runtime. I could then map myLength
into another functor—checking if the length is greater than fifty to produce a boolean functor—all without worrying about a missing string. The code to apply a computation does not depend on the logistics of runtime values.
I am allowed to change what type of functor myString
is without updating any map calls. If myString
is forced to wait on a value from a service, I could turn myString
into a promise without altering the creation of myLength
. There are many functor types. Mapping a functor always returns a functor of the same type, so starting with a promise would create a bunch of promise functors.
There is one guarantee that all functor types are required to make. The code
const isLong = myString
.map(str => str.length)
.map(size => size > 70)
must be equivalent to
const isLong = myString
.map(str => str.length > 70)
Here we see two versions of producing the isLong
functor. In the first version, we map to our length functor, and then we immediately map into our isLong
functor. In the second version, we combine both computations into one function. The second computation is perfectly equivalent to the two separate computations in series; it’s a composite. The second version uses an equivalent computation to the first, and so the functor is required to produce an equivalent isLong
functor as a result.
To put this more explicitly, functors must ensure that sequential calls to map
will return an equivalent functor to a singular call to map with a composite function. $F.map(f).map(g) = F.map(g \circ f).$ This single restrictions creates many indirect restrictions. It means that functors aren’t allowed to reject the results of a computation. Imagine instead of a Maybe functor we used a “anything but a number” functor, which rejects numerical values. The first version with the split computation would fail, whereas the next version would complete. That is not allowed. If the first functor has values at runtime, and you map the functor, it has to create a new functor of the results of sending values through the computation.
Functors are otherwise given leeway in how computation is performed. Depending on the availability of runtime values, computation functions can be run many times, at arbitrary times, or not at all, as long as the functor doesn’t alter or reject the values received from the computation. The List functor runs the map for each of its many values, a Future functor runs the map once its value arrives, and the Stream functor runs the map for its many values as they arrive.
Mapping functors is a completely stateless and deterministic action. A compiler could actually run all of this code without any runtime values. A Maybe functor already has an empty state, so do lists, promises, and streams. If you wished to run the code to create all of the functors for analysis, maybe to see the connections between functors as a flow chart, that would be possible without any mocking of runtime values. This comes down to the code being naturally split between functor creation and computation execution.
See monads next.