Functor is a concept originating from mathematics, to be exact, from a part of mathematics called category theory.
Here I will try to give you some more insight into Functors — what they are, how they work, and what the theory behind them is. I will also implement a simple Functor to better understand how they work and why using them may be a clearer solution.
Additionally as the Functor is the simplest functional container I will use this article as an entry point for new series.
The source code for this article is available in GitHub repository.
Why Even Care About Functors?
Firstly, Functors are probably the simplest of commonly known functional containers. Understanding their mechanics can help while working with more complex containers like Applicatives or Monads and greatly ease your journey deeper into functional programming.
Secondly, it is quite a handy concept, as in fact it is just a mapper and can be used in variety of places and situations. Finally, thanks to its laws Functor is especially helpful when you need to perform operations over some value inside the container.
It may come in handy someday and make your life easier.
What Is Functor?
Functor is a concept from category theory and represents the mapping between two categories. In software, Functors can be viewed as a mapping classes (mappers) that allows us to perform an operation over values inside a wrapper.
Despite being quite easy to implement with the use of a simple interface, we must keep in mind that to honestly call our piece of code a Functor, we have to express certain behaviors described by Functor laws from category theory.
Also, there are no good built-in counterparts of Functors in Java and other modern-day programming languages. Probably the best counterpart can be found in the Scala library called Cats.
Functor Laws
As most of the concepts moved from mathematics to programming, Functors also have some theoretical background behind them. In their case a part of this background are Functor Laws, namely Identity and Associativity.
Below, I will try to present them in a more illustrative way with the use of a Referential Functor class of my own making.
Assumptions about functions used in the associativity part:
- f is a function mapping from type A to type B
- g is a function mapping from type B to type C
- Identity
Mapping values inside the Functor with the identity function should always return an unchanged value.ReferentialFunctor identity = new ReferentialFunctor<>(x).map(Function.identity()); assert identity.valueEquals(x);
- Associativity
In the chain of function applications, it should not matter how functions are nested.ReferentialFunctor leftSide = new ReferentialFunctor<>(x).map(f).map(g); ReferentialFunctor rightSide = new ReferentialFunctor<>(x).map(f.andThen(g)); assert leftSide.equals(rightSide);
Creating a Functor
I will start with the creation of a parameterized type M<T>, a wrapper for the value of type T inside. Then I will have to implement only one method:
- map responsible for performing operations. Here one can pass a function perform mapping operation on value inside the wrapper. This method should have the following signature M (T -> R).
Now I can start implementing a Functor.
Implementing a Functor
package org.pasksoftware.functor.example;
import org.pasksoftware.functor.Functor;
import java.util.function.Function;
public class WrapperWithFunctor implements Functor {
private final T value;
public WrapperWithFunctor(T value) {
this.value = value;
}
@Override
public WrapperWithFunctor map(Function<T, R> f) {
return new WrapperWithFunctor<>(f.apply(value));
}
// For sake of asserting in Example
boolean valueEquals(T x) {
return value.equals(x);
}
}
Here we are with ready Functor implementation, time to explain what happened in these 20 lines of code.
The base of our implementation is the parameterized class with the immutable field named “ value”, which is responsible for storing our value. Then, we have a constructor, which actually makes it possible to instantiate our small Functor implementation.
Next, we have the meat of this snippet, the map method that will guarantee that we are able to fulfill the required conditions in the form of the Laws.
With all the code described, it is time for an example of Functors at work.
package org.pasksoftware.functor.example;
import java.util.function.Function;
public class Example {
public static void main(String[] args) {
int x = 2;
Function<Integer, String> f = Object::toString;
// Task: mapping over the value inside the container object.
// Non-functor
Wrapper wrapper = new Wrapper<>(x);
String mappedValue = f.apply(wrapper.value);
// One liner - Wrapper mappedWrapper = new Wrapper<>(f.apply(x));
Wrapper mappedWrapper = new Wrapper<>(mappedValue);
// Functor
WrapperWithFunctor wrapperFunctor = new WrapperWithFunctor<>(x);
WrapperWithFunctor mappedWrapperFunctor = wrapperFunctor.map(f);
assert mappedWrapperFunctor.valueEquals(mappedWrapper.value);
System.out.println("Values inside wrappers are equal");
}
}
In the above snippet, you could see why Functors can be useful apart from their somewhat simple structure. You can notice that performing the same operations without using Functors required some changes in an API design, exposure of the object state and some additional boilerplate to make it more readable.
Closing Thoughts
Functors can be quite useful even besides their simple structure. Functor-based approach can provide a more descriptive and clearer solution for operating on values inside containers of any type.
Thank you for your time.
Comments are closed.