Toby Hobson

What is a Functor?

banner.png
What is a Functor?
Estimated reading time: 7 minutes

You’re probably familiar with the map method in the Scala standard library. Collections, Futures and Options all have a map method but unfortunately there’s no base class for mappable types, making it hard to write generic code

Cats’ Functor type class allow us to write generic code that can be used for Futures, Options, Lists and more

You can find complete examples of the concepts discussed on my blog in my Github repo . In particular, check out the Functors example

What is a type class?

If you’re new to the concept of type classes I suggest you read my other article explaining them. The Cats library makes extensive use of type classes and a basic understanding is a prerequisite for this article

Cats

Cats is a functional programming library which supports many advanced functional programming paradigms borrowed from languages such as Haskell. However you don’t need a detailed understanding of category theory or functional programming to get value from cats. In my own experience most people (myself included) benefit mostly from the more simple abstractions.

Cats defines type classes for various functional concepts along with implementations for common types. Of course, being type classes you can write your own implementations if a particular type is not supported “out of the box”. Today we’ll be looking at one of the most basic type classes - the Functor

Getting started with cats is pretty simple. At the time of writing the latest stable release is 1.0.1 and you can add it to your SBT build as usual:

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

What is a Functor?

In simple terms any type constructor (type wrapping another type) that has a map method can be thought of as a Functor i.e. List, Option, Future etc. 1 The problem is that although we know List, Option and Future all have a map method, the standard library has no base type/trait to represent this so we can write:

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

but we can’t write:

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

The Cats Functor type class

However using cats’ Functor type class we can write such a method:

import cats.Functor

case class LineItem(price: Double)

def withVat[F[_]](order: F[LineItem])(implicit ev: Functor[F]): F[LineItem] = {
  Functor[F].map(order)(o => o.copy(price = o.price * 1.2))
}

Let’s decode the method signature. The method is parameterised based on a type of F[_]. This means any type constructor e.g. Option, List, Future etc. At this stage we haven’t specified anything about F needing a map method. The parameter itself is of type F[LineItem] i.e. any type wrapping a LineItem. Finally we have the implicit parameter ev: Functor[F] which means we must have a type class implementation in place which allows us to treat F as a cats Functor.

In the method body we call Functor[F].map(...) i.e. we create a Functor for F using the implicit evidence and call it’s map method. Our method should now compile but if we try to call it we’ll run into a problem:

val lineItems = List(LineItem(10.0), LineItem(20.0))
withVat(lineItems).foreach(println)

Error:(15, 12) could not find implicit value for parameter ev: cats.Functor[List]

The compiler is telling us that we need to supply the evidence that List is a cats.Functor i.e. we need a type class implementation for List. The cats library includes such an implementation already so we can just use this:

import cats.Functor
import cats.instances.list._

def withVat[F[_]](...)

val lineItems = List(LineItem(10.0), LineItem(20.0))
withVat(lineItems).foreach(println)

So far so good, lets try to use an Option instead of a List. Again we’ll need to pull in the type class implementation for Option:

import cats.Functor  
import cats.instances.list._
import cats.instances.option._

val maybeLineItem = Some(LineItem(10.0))
withVat(maybeLineItem).foreach(println)

Error:(16, 12) could not find implicit value for parameter ev: cats.Functor[Some]

Hmmm, seems it didn’t work. What went wrong? Actually we’ve run into an issue of variance. Cats includes an implementation for Option but we’re passing Some. We might expect that Functor[Some] would be treated as Functor[Option] (known as Covariance) but in fact Cats is generally invariant of types i.e. it wants an Option and only an Option, a Some or None won’t do. We need to tell the compiler to treat our Some as an Option:

val maybeLineItem: Option[LineItem] = Some(LineItem(10.0))
withVat(maybeLineItem).foreach(println)

As you work with cats you’ll see this is a common theme so remember this compiler trick - you’ll need it again for sure.

This hack is quite annoying and it comes up again and again. Fortunately cats includes a helper. Firstly import cats.syntax.option._ then write something like val maybeLineItem = LineItem(10.0).some. The .some call is equivalent to Some(LineItem(10.0)) but it tells the compiler to treat the Some as an Option

Why is all this significant?

By using the Functor type class we can abstract over anything that can be mapped. We’re not restricted to the types in the standard library, we could add a map method to our own types. So long as we write a Functor type class implementation for it we would pass it to our withVat function above. So why is this significant?

Well firstly it may be useful to have some generic code that can handle Options, Futures, Lists etc but the real power comes when we need to test it. Take our original withVat function that accepts Futures:

def withVat(eventualOrder: Future[LineItem]): Future[LineItem] = ???

To test this we need to create an instance of a Future and then block/await for the result or otherwise use something like Scalatest’s async specs. It’s a bit nasty. By accepting a generic Functor we can pass an option in the test:

def withVat[F[_]](order: F[LineItem])(implicit ev: Functor[F]): F[LineItem] = ???
...
withVat(LineItem(10.0).some) shouldBe LineItem(12.0).some

cats also includes a helper Type called Id which lets us pass “raw” types in e.g.

import cats.Id
...
withVat(LineItem(10.0): Id[LineItem]) shouldBe LineItem(12.0)

It’s just another compiler trick with tells the compiler to treat A as F[A]

Simplifying the code

Cats also introduces a concept of syntax or extension methods. This concept is implemented using implicit classes and allows us to write order.map(...) instead of Functor[F].map(order)(...). We can also drop the implicit parameter by specifying that type F[_] is a Functor

...
import cats.syntax.functor._

def withVat[F[_]: Functor](order: F[LineItem]): F[LineItem] = {
  order.map(o => o.copy(price = o.price * 1.2))
}

withVat adds VAT of 20% to LineItems but we can build a higher order function which can deal with any type:

def withFunctor[A, B, F[_]: Functor](order: F[A])(op: A => B): F[_] = order.map(op)
...
val lineItems = List(LineItem(10.0), LineItem(20.0))
withFunctor(lineItems)(_.price * 1.2).foreach(println)

Of course this is a contrived example as withFunctor adds no value over a simple inline call but it illustrates the point that A, B & F can be anything so long as the caller of the method:

  • Supplies evidence that F is a Functor (an implementation)
  • Knows how to map from A to B

A Different view

All the examples I have given so far assume we want to write generic methods capable of handling Lists, Options, Futures etc and this is certainly a common use case for Functors. However we can also use functors inline in our code and this is especially useful when composing them. A common pattern we often see is something like:

val order: Future[Option[Order]] = fetchOrder(...)
order.map(_.map(applyVat))

The nested map call is messy but as cats includes Functor implementations for both Future and Option we can compose them:

import cats.Functor  
import cats.instances.future._
import cats.instances.option._
...
Functor[Future].compose[Option].map(order)(applyVat)

We can cut down the boilerplate by writing our own implicit class which adds a nestedMap method to all nested Functors

implicit class RichFunctor[A, F[_]: Functor, G[_]: Functor](underlying: F[G[A]]) {
  def nestedMap[B](op: A => B): F[G[B]] = Functor[F].compose[G].map(underlying)(op)
}

val order: Future[Option[Order]] = fetchOrder(...)
// use the new implicit method we defined above
order.nestedMap(applyVat)

This will work for Future[Option[A]] but it will also work for any combination of type constructors so long as we have the Functor type class implementations in scope:

...
import cats.instances.future._
import cats.instances.list._

val orders: Future[List[Order]] = fetchOrder(...)
orders.nestedMap(applyVat)

What next?

I’ve covered map now it’s time to cover flatMap. To do so you need to read about Monads

Laws

I’m oversimplifying things a bit here. For a typeclass to be a Functor it must obey two laws: Firstly it should be possible to compose two map calls. Secondly mapping with the identity function should have no effect. You can read more on the cats website

comments powered by Disqus

Need help with your project?

Do you need some help or guidance with your project? Reach out to me (email is best)