In this text, the final summary of my categories theory series, I will use my spotlight and take a closer look at the relations between all three previously described functional containers, namely: Functor, Monad, and Applicative.
Below, you will find a comparison of them in terms of:
- Theory
- Laws
- Methods
- Possibilities & Use Cases
Theory
Functor
In category theory, a Functor represents the mapping between two categories.
In software, Functors can be viewed as a util class that allows us to perform a mapping operation over values wrapped in some context.
It can also be viewed as an interface proving a certain ability, namely the ability to transform the state of an object. In more functional focused languages like Scala or Haskell, Functor is a typeclass.
There are a few types of Functors. However, their in-depth description and analysis is quite out of the scope of this article, but do not be afraid – I added links to other resources that will help you deepen your knowledge.
Functor types:
There are no good built-in counterparts of Functors in Java and other modern-day programming JVM languages. Nevertheless, they are reasonably easy to implement manually. Probably the best counterpart can be found in the Scala library called Cats or in Haskell.
Applicative
In category theory, an Applicative, also known as an Applicative Functor, is a concept that generalizes a Functor by allowing for operations not just on the values inside a context but also on functions within a context.
It sits between Functors and Monads in terms of expressive power. While less powerful than Monads, Applicatives are more structured than Functors.
By the book, Applicatives do not allow chain operations in the same ways as Monads do – with the output of one operation being the input of the other. On the other hand, unlike Functors, Applicatives allow us to sequence our computations.
Unfortunately, such a relation is quite hard to achieve in the world of software as in the end, the underlay data will end up being used as input for the next.
Monad
In the world of software, we can view it as a wrapper that puts our value in some context and allows us to perform operations, specifically operations that return results wrapped in the Monadic context, on the value.
Monads are mostly used to handle all kinds of side effects:
- Performing I/O operation
- Handling operations that can throw exceptions
- Handling async operations
Another important point is that we can chain the operations in such a manner that the output of an operation at any step is the input to the operation at the next step. Such behavior allows us to write very declarative code with minimal boilerplate.
Contrary to Functor, Monad has quite a lot of implementations built in modern-day programming languages:
- Scala (Option, Try, Either, … and more)
- Haskell (IO, …. and more ),
- Java (Optional, not exactly a Monad, but close)
From both practical and theoretical points of view, it is also the most powerful of all the three concepts. The complete hierarchy of all three in terms of pure mathematical power looks more or less like this, going from least to most powerful:
Laws
Functor
If we want to implement a Functor, our implementation needs to obey two laws: Identity and Associativity
1. Identity
Mapping values inside the Functor with the identity function should always return an unchanged value.
2. Associativity
In the chain of function applications, it should not matter how functions are nested.
Applicative
If we want to implement Applicative, our implementation needs to obey four laws namely: Identity, Homomorphism, Interchange, Composition.
1. Identity
Applying the identity function to a value inside the context should always return an unchanged value.
2. Homomorphism
Applying a function in context to a value in context should give the same result as applying the function to the value and then putting the result in context with the usage of pure.
3. Interchange
Applying the function with context f to a value with context should be the same as applying the wrapped function, which supplies the value as an argument to another function, to the function with context f.
4. Composition
Applying the function with context f and then the function with context g should give the same results as applying functions composition of f and g together within the context.
Alternative Applicative
There is also an equivalent version of Applicative whose implementation needs to obey three laws: Associativity, Left Identity, and Right Identity.
1. Left identity
If we create a new Applicative and bind it to the function, the result should be the same as applying the function to the value.
2. Right identity
The result of binding a unit function to the Applicative should be the same as the creation of a new Applicative.
3. Associativity
In the chain of function applications, it should not matter how functions are nested.
If you would like to take a closer look at the laws of this version of Applicative you can notice that they are the same as the Functor laws. The change in the Identity Law originates from the difference in the setup of both concepts. In particular from the existence of the pure method in Applicative, in a way, we have to check one more case.
Monad
If we want to implement a Monad, we have to obey three laws: Left Identity, Right Identity, and Associativity.
1. Left identity
If we create a new Monad and bind it to the function, the result should be the same as applying the function to the value.
2. Right identity
The result of binding a unit function to a Monad should be the same as the creation of a new Monad.
3. Associativity
In the chain of function applications, it should not matter how functions are nested.
Same as in the case of Applicative, Monads Laws are very similar to Functor laws. What is more, Monad Laws are exactly the same as Applicative Laws, the only difference is the concept described in the Laws, the condition remains unchanged.
The relation between the three in terms of which concepts extends which will look more or less like in the picture below, basically, everything is a Functor.
Methods
To implement any of the structures, you will need a language with generics support as a parameterized type M<T> is a base for any of them. On the other hand, you can just generate the code for all required types via a macro or some other construct but with generics it is significantly easier.
Functor
To implement Functor, you will have to implement only one method:
- map, you pass a function that operates on value in context. This method should have the following signature M<U> (T ->U).
Applicative
To implement Applicative you will have to implement two methods:
- pure, which is used to wrap your value in the Applicative context and has the following signature M<T>(T).
- apply (ap), you pass a function with context, the function then operates on the value in the context. This method should have the following signature M<U> (M<T -> U>).
Alternative Applicative
There is also an equivalent version of Applicative where we are using the product instead of apply.
- product, you pass two different values wrapped in Applicative context and get context with both values in return. This method should have the following signature M<(T, U)>(M<T>, M<U>).
In both approaches to Applicatives, you get a map method with signature M<U> (T -> U) by their definition as all Applicatives are Functors, you.
Monad
To implement Monad you will have to implement two methods:
- unit (of,) which is used to wrap the value in Monad context and has the following signature M<T>(T).
- bind (flatMap), you pass a function that operates on value in context but the result is already wrapped in Monad context. This method should have the following signature M<U> (T -> M<U>).
What is more, Monad is also an Applicative and, by extension, a Functor. It means that we are getting a lot of additional methods for free. In particular, a map method from Functor and apply or product methods from Applicative.
The possibilities & use cases
Functor
The Functor is just a util method. It does not provide anything more than simple mapping of a value inside an arbitrary context.
On the other hand, it is also the simplest and the easiest of all three concepts and can be used almost anywhere where we need to perform operations over the values in the context.
Thought Functors still have their limitations. By its definition, Functor is unable to chain computations. What is more, Functor does not provide a way to flatten the result of performed operations, so we can easily end up with nested types like <List<List<Long>>.
However, the possibility of performing effectfull operations without moving them out of the context is quite a good thing.
Applicative
Going further, we have an Applicative with which we can apply functions in context to a value with context. Additionally, with its product version, we can create a tuple out of two objects within the same context.
Such behavior can be extremely useful in some cases – we can use it to compose multiple effects to one effect, reducing types like List<Future> to more reasonable Future<List>.
That is the reason why Applicatives are a great choice for implementing concepts like parsers, traversables, or composers and any other use case where we need to work with many independent effects of the same type.
What is more, because the Applicative is a Functor we can also apply some operations on such output tuples via the map method.
Monad
Last but not least, we have Monad – the most powerful of all three structures.
Each Monad implementation represents an effect:
- emptiness (Option)
- possible failure (Try)
- result or failure of an operation (Either)
- chain of computations (IO)
What is more, Monad gives us the way to put values in such an effectfull context.
Furthermore, thanks to the flatMap method, we can handle nested return types. This in turn solves the issues with M<M<T>> styled types (Optional<Optional<Long>).
Monad implementation will automatically flatten such types to have only one effect – an operation that the map method from Functor is unable to make by its definition.
Additionally, with Monads, we can perform contextual operations as we can pass outputs of one operation as inputs to another achieving a nice, declarative chain of computations.
Summary
Functional containers are a very useful tool that can be applied to a variety of problems. In my opinion, we should find them a place in our engineering toolbox or at least be aware of them.
Just please remember that they are not a silver bullet, and making everything a Monad may not be the best way to write your code.
As a side note, I would like to add that these containers add another nice touch of math to the world of software engineering, which is another good point to like them, at least in my opinion.
Hope that you find my text interesting. Thank you for your time.
Comments are closed.