What is a Functor?
Andrew (he/him)
Posted on November 28, 2021
Photo by ArtHouse Studio from Pexels
Intro
The term functor comes from category theory and describes, in a nutshell, a mapping between two categories of things. In programming, these categories are usually types (for instance the generic type T
), classes (like String
), or kinds (types of types, like F[T]
).
Consider the following function
def map[C, D](c: C)(f: C => D): D = f(c)
This function takes two generic type parameters, C
and D
. It also takes an instance of C
, which we call c
, in the first parameter list.
In the second parameter list, it takes a function from C
to D
, which we call f
.
map
applies the function f
to the parameter c
and returns some value of type D
, since f
maps values of type C
to values of type D
.
A functor is similar to map
, except we're mapping entire categories of things. So we don't want to map a single element c
to a single element of type D
-- we want to map the type C
to the type D
.
Functor
s in Scala
In Scala, this looks like
trait Functor[F[_]] {
def map[C, D](fc: F[C])(f: C => D): F[D]
}
where F[_]
is a higher-kinded type with an existential type parameter, _
.
Existential types in Scala 2 are basically unbounded types, similar to the wildcard type in Java. They have been dropped from Scala 3.
Example
All we've done above is move from c: C
to fc: F[C]
in the first parameter list and "wrap" the return type of map
in this same outer type F
. The above can be a bit confusing, so imagine that F[_]
is a new collection type that we're writing called Improv
(similar to Scala's Option
):
sealed trait Improv[+C] {
import Improv._
def and[D](`then`: C => D): Improv[D] = this match {
case Yes(what) => Yes(`then`(what))
case No => No
}
}
object Improv {
case class Yes[C](what: C) extends Improv[C]
case object No extends Improv[Nothing]
}
We might then write an ImprovFunctor
like
class ImprovFunctor extends Functor[Improv] {
def map[C, D](ic: Improv[C])(f: C => D): Improv[D] = ???
}
The above is a bit clearer -- here we're mapping the type Improv[C]
to a type Improv[D]
, using a function C => D
. If you're familiar with mapping over collection types, you might know that the way we accomplish this is by applying the function f
to every element of ic
, to produce a new collection with elements of type D
.
(Also note that we write
Functor[Improv]
and notFunctor[Improv[_]]
.)
In that case, the implementation of map
could be written like
class ImprovFunctor extends Functor[Improv] {
def map[C, D](ic: Improv[C])(f: C => D): Improv[D] = ic.and(f)
}
For example:
import Improv._
val im = new ImprovFunctor
im.map(Yes(24))(n => s"I just thought of something funnier than $n")
evaluates to
Yes("I just thought of something funnier than 24")
We've map
ped the Improv[Int]
to an Improv[String]
.
Functor Laws
Given the functions
def id[C](c: C) = c
def c2d[C, D](c: C): D = ???
def d2e[D, E](d: D): E = ???
...an object im
is a Functor[F]
if it satisfies the two functor laws for any fc: F[C]
:
-
im.map(fc)(id)
==
id(fc)
-
im.map(im.map(fc)(c2d))(d2e)
==
im.map(fc)(c => d2e(c2d(c)))
Let's investigate these laws using the ImprovFunctor
.
The first functor law requires the following concrete example to be true
// fc == Yes(24)
assert {
im.map(Yes(24))(id) == id(Yes(24))
}
...and the second functor law requires the following concrete example to be true
def c2d(c: Int): Double = c + 1.0
def d2e(d: Double): String = s"$d!"
assert {
im.map(im.map(Yes(24))(c2d))(d2e) ==
im.map(Yes(24))(c => d2e(c2d(c)))
}
These are of course just specific examples using a specific class and so they don't prove that ImprovFunctor
is a functor, but, provided that our definition of a Functor
in Scala is accurate
trait Functor[F[_]] {
def map[C, D](fc: F[C])(f: C => D): F[D]
}
...the Scala compiler can determine that our ImprovFunctor
implementation conforms to this interface.
Posted on November 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.