Functors in Functional Programming

Functors in Functional Programming
Knoldus Blog Audio
Reading Time: 5 minutes

1. Overview

In this tutorial, we’ll take a look at Functor type class in Cats. The idea of Functor is “something that can be mapped over”, we’ll see what is actually mapped and how. In functional programming, Functors come into play when we have types or values wrapped inside contexts or containers. We don’t have to know any of the implementation details of those contexts or containers themselves.

2. SBT Dependencies

To start, let’s add the Cats library to our dependencies :

libraryDependencies += "org.typelevel" %% "cats-core" % "2.2.0"

Here we’re using version 2.2.0 of the Cats library.

3. What is a Functor?

In terms of functional programming, a Functor is a kind of container that can be mapped over by a function. It is basically an abstraction that allows us to write generic code that can be used for Futures, Options, Lists, Either, or any other mappable type.

In simple terms, any type that has a map function defined and preferably an “identity function” is a Functor.

Standard library has no native base trait/type to represent this so we can write :

def calcBudget(orders: List[LineItem]) = orders.map(...) 
def calcBudget(maybeOrder: Option[LineItem]) = maybeOrder.map(...) 
def calcBudget(eventualOrder: Future[LineItem]) = eventualOrder.map(...)

But we can’t write it generically like this :

def calcBudget(order: Functor[LineItem]) = order.map(...)

4. The Cats Functor Type Class

However using cats’ Functor type class we can write a generic code that can be used for Futures, Options, Lists, Either, or any other mappable type.

But before that, let see definition of Functor type class. A Functor is defined as F[A] with a map operation (A => B) => F[B] :

package cats trait Functor[F[_]] { 
def map[A, B](fa: F[A])(f: A => B): F[B]
}

Now let’s rewrite calcBudget using Functor :

import cats.Functor case class LineItem(price: Double) def calcBudget[F[_]](order: F[LineItem])(implicit functorEvidence: Functor[F]): F[LineItem] = { 
Functor[F].map(order)(o => o.copy(price = o.price * 1.2))
}

Let’s decode this method signature.

The calcBudget method is parameterized based on a type of F[_]. Here F[_] means any mappable type e.g. Option, List, Future, etc. The parameter order itself is of type F[LineItem] i.e. any type wrapping a LineItem.

The implicit parameter functorEvidence: Functor[F] means that we must have a type class implementation, which allows us to treat F as a Functor.

In method body, we call Functor[F].map() i.e. we create a Functor for F using implicit evidence functorEvidence and call it’s map method.

5. Functor Laws

If we’re creating our own Functors, then those Functors have to respect some rules, called Functor’s Laws :

5.1. Identity Law

When a Functor is mapped over with identity function (the function returning its parameter unchanged), then we must get back the original Functor (the container and its content remain unchanged).

Functor[X].map(x => identity(x)) == Functor[X]

5.2. Composition Law

When a Functor mapped over the composition of two functions, then it should be same as mapping over one function after the other one.

Functor[X].map(f).map(g) == Functor[X].map(x => g(f(x))

6. Examples of Functors in Scala

In Scala, we are aware of Functor’s map function to work with effects. Cats provide various type class implementations for Functors, for predefined types like Lists, Futures, Options, Either, etc.

6.1. List as a Functor

Now, let’s see how List act as a Functor :

List is considered as a Functor as it has a map method. While iterating over List using its map method, we should think of it as transforming all of the values inside in one go, without changing the structure of the List.

We can implement the above concept using following example :

import cats.Functor object ListFunctor { 
def transformList(list: List[Int]): List[Int] = {
Functor[List].map(list)(_ * 2)
}
} val list: List[Int] = List(1, 2, 3, 4, 5)
val transformedList = List(2, 4, 6, 8, 10)
assert(ListFunctor.transformList(list) == transformedList)

6.2. Option as a Functor

Now, let’s see how Option act as a Functor :

Option is also considered as a Functor as it also has a map method. When we map over an Option, we transform the contents but leave the Some or None context unchanged.

We can implement the above concept using following example :

import cats.Functor object OptionFunctor { 
def transformOption(option: Option[Int]): Option[String] = {
Functor[Option].map(option)(_.toString)
}
} val option: Option[Int] = Some(10)
val transformedOption = Some("10")
assert(OptionFunctor.transformOption(option) == transformedOption)

6.3. Either as a Functor

Now, let’s see how Either act as a Functor :

Either is also considered as a Functor as it also has a map method. When we map over an Either, we transform the contents but leave the Left or Right context unchanged.

We can implement the above concept using following example :

import cats.Functor object EitherFunctor { 
def transformEither(either: Either[Int, String]): Either[Int, Int] = {
Functor[Either].left.map(either)(_.size)
}
} val either: Either[Int, String] = Left("Baeldung")
val transformedEither = Left(8)
assert(EitherFunctor.transformedEither(either) == transformedEither)

6.4. Future as a Functor

Now, let’s see how Future act as a Functor :

Future is a Functor that sequences asynchronous computations by queueing them and applying them as their predecessors complete. The type signature of its map method has the same shape as the signatures above. However, the behavior is very different. In case of Future, the wrapped computation may be ongoing, complete, or rejected. If the Future is complete, our mapping function can be called immediately. If not, some underlying thread pool queues the function call and comes back to it later. We don’t know when our functions will be called, but we do know what order they will be called in. In this way, Future provides the same sequencing behavior seen in List, Option, and Either.

We can implement the above concept using following example :

import cats.Functor object FutureFunctor { 
def transformFuture(future: Future[Int]): Future[Int] = {
Functor[Future].map(future)(_ + 1)
}
} val future: Future[Int] = Future{10}
val transformedFutureResult = 11
FutureFunctor.transformFuture(future).map(result => assert(result == transformedFutureResult))

7. Significance of Functors

By using Functors we’re not restricted to the types in the standard library, thus we can abstract over anything that is mappable. We could define a map method to our own types by using the concept of syntax or extension methods. This concept allows us to write order.map(…) instead of Functor[F].map(order)(…). We can also drop the implicit evidence parameter by specifying that type F[_] is a Functor :

import cats.syntax.functor._ //for map def calcBudget[F[_]: Functor](order: F[LineItem]): F[LineItem] = {
order.map(o => o.copy(price = o.price * 1.2))
}

But we can also build a higher-order function that can deal with any type and perform any mapping operation :

def withFunctor[A, B, F[_]](item: F[A], op: A => B)(implicit functorEvidence: Functor[F]): F[_] = Functor[F].map(item)(op) val lineItemsList = List(LineItem(10.0), LineItem(20.0)) 
val result = FunctorSyntax.withFunctor(lineItemsList, calcBudget)
assert(result == List(LineItem(10.0), LineItem(20.0)))

In above example, A, B & F can be anything so long as the caller of the method :

  • Supplies evidence that F is a Functor
  • Knows how to map from A to B

8. Summary

In this article, we’ve looked at Functors provided by Cats library of Scala. Functor is a kind of container that provides mapping function to represent sequencing behaviors. We talked about what is the need of Functors and how can we define them. Then we learned about two laws that a Functor has to respect. Then we saw that we can write Functor type class implementation for all the mappable types present in Scala ecosystem like Futures, Options, Lists, Either, etc. We’re not restricted to the types in the standard library. We can also define map method to our own types by using the concept of syntax or extension methods.

All the code implementations are available over on GitHub.


Similar articles –

You can also checkout my other articles on Scala Cats series —

Leave a Reply