By Luis Atencio
In essence, a functor is nothing more than a data structure you can map functions over with the purpose of lifting values from a container, modifying them, and then putting them back into a container. Simply put, it is a design pattern that defines semantics for how fmap should work. Here’s the general definition of fmap:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B) //#A
#A - Where Wrapper is any container type
The function fmap takes a function (from A -> B) and a functor (wrapped context) Wrapper(A) and returns a new functor Wrapper(B) containing the result of applying said function onto the value and then closes it once more. Here’s a quick example using the increment function as our mapping function from A -> B (except in this case A and B are the same types):
Figure 1 A value of 1 is contained within a container W, the functor is called with said wrapper and the increment function, which transforms the value internally and closes it back into a container.
Notice that because fmap basically returns a new copy of the container at each invocation, it can be considered to be immutable.
A discussion on functors can easily get very formal and theoretical. If you do a quick web search for functors, you will find articles that will bombard you with terms such as: morphism and categories. The reason for this is that, like all functional programming techniques, functors originate from mathematics—in this case, category theory.
Without getting into the weeds, I can explain the basic meaning of this. Functors are defined as: “morphisms between categories.” All this really means is that a functor is an entity that defines the behavior of (fmap) that, given a value and function (morphism), maps said function onto a value of certain type (category) and generates a new functor.
Indeed, this is a bit theoretical to understand. Let’s go over a very simple example. Consider a simple 2 + 3 = 5 addition using functors. I can curry a simple add function to create a plus3 function as such:
var plus = R.curry((a, b) => a + b); var plus3 = plus(3);
Now I will store the number two into a simple Wrapper functor:
var two = wrap(2);
Calling fmap to map plus3 over the container performs addition:
var five = two.fmap(plus3); //-> Wrapper(5) //#A five.map(R.identity); //-> 5
#A - Returns the value inside a context
The outcome of fmap yields another context of the same type, which I can map R.identity over to extract its value. Notice that, because the value never escapes the wrapper, I can map as many functions as I want onto it and transform its value at every step of the way:
two.fmap(plus3).fmap(plus10); //-> Wrapper(15)
This can tricky to understand, so here’s is a visual of how fmap works again with plus3 in figure 2:
Figure 2 The value 2 has been added to a Wrapper container. The functor is used to manipulate this value, by first unwrapping it from the context, applying the given function onto it, and re-wrapping the value back into a new context.
The purpose of having fmap return the same type (or wrap the result again into a container) is so that we can continue chaining operations. Consider the following example that maps plus on a wrapped value and logs the result as shown in listing 1:
Listing 1 Chaining functors to apply additional behavior onto a given context
var two = wrap(2);
two.fmap(plus3).fmap(R.tap(infoLogger)); //-> Wrapper(5)
Running this code prints the following message on the console:
InfoLogger [INFO] 5
Does this idea of chaining functions sound familiar? Actually, you’ve been using functors all along without realizing it. This is exactly what the map and filter functions do for arrays:
map :: (A -> B) -> Array(A) -> Array(B) filter :: (A -> Boolean) -> Array(A) -> Array(A)
Functions map and filter are “homomorphism between categories.” The reason being is that both functions preserve the same type:
- homo: same
- morphism: a function that maintain structure
- category: type of value contained
Extending this concept into functions, consider another type of a homomorphic functor you’ve seen all along: compose. As you may know, the compose function is a mapping from functions into other functions:
compose :: (B -> C) -> (A -> B) -> (A -> C)
Functors, like any other functional programming artifact, are governed by some important properties:
- They must be side effect free: mapping the R.identity function can be used to obtain the same value over a context. This proofs they are side effect free and preserves the structure of the wrapped value.
wrap('Get Functional').fmap(R.identity); //-> Wrapper('Get Functional')
- They must be composable: this property indicates the composition of a function applied to fmap should be exactly the same as chaining fmap functions together. As a result, the following expression is exactly equivalent to the program in listing 1: two.fmap(R.compose(plus3, R.tap(infoLogger))).map(R.identity); //-> 5
Structures such as functors are prohibited from throwing exceptions, mutating elements on a list, or altering a function’s behavior. Their practical purpose is to create a context that allows you to securely manipulate and apply operations to values, without changing the original value. This is evident in the way map transforms one array into another without altering the original array; this concept equally translates to any container type.
By Luis Atencio