It is often the case when programming that I have a function that takes some input argument and returns a transformed output. I also might have some piece of data I wouldd like to apply that function to, but I can't because the data is encapsulated inside something else. I'll call the "data" in this case the "target" and the "something else" the "context". To make it concrete, I might have some way to get the number of completed todo items but the only way to get the todo items is via a selector function, because they are stored in a global store using Redux.
const todoSelector = (state, props) => state.todos; const lengthOfTodos = (todos) => todos.length;
Note how I can't just apply the lengthOfTodos
function to the todoSelector
. The lengthOfTodos
function expects an array of todo objects, and the todoSelector
is a function. Somehow I need to apply lengthOfTodos
to the result of todoSelector
. I don't have the arguments to pass to todoSelector
though to get the result! This is where Functor comes to help. With Functor I can define that lengthOfTodos
is to be applied to the target data in the context of the selector function. This is what it looks like.
Functor.map(lengthOfTodos, todoSelector)
Simple!
A Functor is really best talked about as an interface that an entity can implement. That interface is pretty simple. It is simply the map function, along with some properties on how that function behaves. The specification is below.
{import-types-consumer, 2:2}
export type Functor<Static, In, OutA, OutB> = {
map: ((OutA => OutB), Consumer<Static, In, OutA>) => Consumer<Static, In, OutB>
};
So a Functor is an object that implements a map property. That property must be a function that takes two arguments: a function that transforms from one type to another, and a consumer that outputs the first output state. It returns a consumer that outputs the second output state. In other words, it allows the transformation of the consumer output without knowledge of the specific consumer.
The map function has a few properties that make it convenient to work with. The first property is called the Identity property. It simply says that if the transformer is the identity function, the target doesn't change.
const identity = x => x; Functor.map(identity, todoSelector) === todoSelector;
The second property is known as the Composition property. This property says that if I have two transformers I want to apply to a target, applying the composition of those transformers is the same as applying each transformer individually one after the other.
{compose, 12:3}
const transformerA = todos => todos.emptyTodos;
const transformerB = todos => todos.length;
Functor.map(compose(transformerB, transformerA), todoSelector) ===
Functor.map(transformerB, map(transformerA, todoSelector));
The consequence of this is that I don't need to worry about how I call map
when I have multiple transformations. I can call it once, twice, or as many times as makes sense. I can also always refactor to build up one large transformer pipeline and only call map once.
The implementation is relatively simple, but can be tricky for those who aren't used to functional programming or the Functor concepts. Remember that the context is selector functions, which have a specific shape.
map: function(transformer, context) { return (state, props) => transformer(context(state, props)); }
It needs to return a value in the same context, so the return value is a selector function that takes state and props as arguments. That returned function though, applies the transformer that was passed into map to the returned value of evaluating the context with the given state and props. Thus I get a new context that contains the transformed value.
This does obey the laws, but I leave it as an exercise to the reader to reason through them and prove it themselves.
Finally I need to wrap it all up in a nice module.
The Functor implementation must import the Functor type definition.
Other modules need to import Functor.