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
const nextNum = myNum + 1
const nextNumFn = myNumFn.map(n => n+1)
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.
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,
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.