1. Overview
Functional Programming can be a new concept for Java developers who are starting their journey into the Scala world. It’s not enough just to learn the language syntax itself. To understand the whole picture, the developer should learn new types of abstractions, idioms, and when and how to use them.
In this tutorial, we’ll look at functor abstraction and how it’s useful. It’s a common pattern we can find around the core Scala library, even if we didn’t know it was the functor itself.
2. Container Types
A functor is a mapping between categories. The name and the concept come from the mathematical theory that aims to find and abstract away generic patterns of other branches of mathematics.
Mapping can refer to the transformation of data or to the function. As we know, the function in programming is a set of instructions that transforms an input value into an output value. The name “functor” sounds similar to the name “function” and has almost the same meaning of transformation.
A proper function is a way to transform the value of one type into another. In mathematics, it’s written as f: A => B. We couldn’t just apply a proper function to some value inside a container.
A container or context is something all Scala developers encounter. The List[A] is a container that can hold zero or any number of values of type A. It allows for the abstraction of functionality to manipulate such a sequence independently from the type of values the container is holding.
The same is true for other containers like Option[A], Future[A], Either[A], and Map[A, B] as well. They are all encapsulating some generic logic to manipulate any type of data.
To manipulate the data inside a container in a non-functional way would require us to unpack a value or a set of values, apply our function, and pack the results back into the container. Let’s say we want to manipulate the value inside the Option container:
var patientIdOpt = Some("patient 0")
if (patientIdOpt.isDefined) {
val patientId = patientIdOpt.get
patientIdOpt = Some(s"$patientId is cured")
}
assert(patientIdOpt.contains("patient 0 is cured"))
The simple task of applying a function takes many more steps in an imperative approach.
If we have a function to manipulate the values of proper type A => B, and a container C[A], there is a way to apply any proper function to the value inside this container without the need to take out the inner type from the container, and then pack the results back.
3. Functor Behavior in the Core Library
The functor is an answer. Despite the fact that the functor is not a part of the language abstractions, all containers have functor properties. Functors allow us to transform the value of one type to another while respecting the container context.
Any container from the core library has a common way to apply any function to the value inside the container, within the boundaries of a context. If an Option container holds no value, the result of applying a function to a None is a None itself. If the List contains more than one value, the function is applied to all of its values:
val temperatures = List(38, 39, 39, 37)
val panadol: Int => Int = temperature => temperature - 10
val lowTemperatures = temperatures.map(panadol)
assert(lowTemperatures == List(28, 29, 29, 27))
The “magic” is inside the function map(). Its definition is the same for all containers:
trait F[A] {
def map[A,B](f: A => B): F[B]
}
The F[A] is a container inside which the map() function is defined. f: A => B is a proper function to apply on the value inside a container, and F[B] is a resulting container with the resulting value of function application.
4. Higher-Kinded Functor
Scala’s rich Type System allows defining a functor more generically, abstracting away a container type itself. External libraries such as Scalaz or Cats provide their own implementations of a higher-kinded Functor[F[_]].
For example, the Cats library defines a trait Functor[F[_]] itself and the implementations for the common containers. Let’s use a functor for Option.
First of all, we should add a dependency inside our build.sbt file:
scalacOptions += "-Ypartial-unification"
libraryDependencies += "org.typelevel" %% "cats-core" % "2.4.2"
Then let’s import Cats classes:
import cats.instances.option._
import cats.Functor
Now we can define a test data to map over:
val option = Some("Hello!")
And use map functionality of the functor:
val result = Functor[Option].map(option)(_.toUpperCase)
assert(result == Some("HELLO!"))
Libraries’ implementations respect a set of laws that Category Theory brings in with the functor itself.
5. Functor Laws
So, is any container that has a similar map() function a functor? The answer is “almost”. To be a proper functor, a container’s map() function should obey two laws:
- Composition — Applying map() on the function f(), and then applying map() on the other function g(), should return the result, similar to applying map() once but on the composition of the functions: f() and g(). fa.map(f).map(g) = fa.map(f.andThen(g))
- Identity — Applying map() on the identity() function returns the same container without any changes: fa.map(x => x) = fa
6. Conclusion
In this article, we showed the instrument of abstractions in Scala and the role of the functor in this chain. Along with the other abstractions such as Applicative Functor and Monad, we have a rich set of instruments for applying the functional approach to developing applications.
As always, all examples from the topic can be found over on GitHub.