Category mania! Are we paid for speaking about it? I’m afraid we don’t. Would we like to? Very likely. In previous posts we spoke about monoids and monads, but now it’s functor’s time. Functors are everywhere and we can prove it. Before reviewing its different kinds, let’s focus on key concepts.
What’s a morphism?
It’s just a transformation between two structures, a change between spaces, a mutation.
In Scala? A simple function:
val f: A => B
What’s a functor?
Quick and simple answer: the behavior that defines, for a certain F[A] type constructor, the map method.
trait Functor[F[_]]{ def map[A, B](fA: F[A])(f: A => B): F[B] }
There are functors for List
, Option
, Future
, Try
, …
object ListFunctor extends Functor[List]{ def map[A, B](fA: List[A])(f: A => B): List[B] = fA match { case Nil => Nil case (head :: rest) => f(head) +: this.map(rest)(f) } } object OptionFunctor extends Functor[Option]{ def map[A, B](fA: Option[A])(f: A => B): Option[B] = fA match { case None => None case Option(a) => Option(f(a)) } } //...
Actually, apart from the famous map
method, they should fit into a couple of properties:
- Identity morphism:
F[Id[A]] = Id[F[A]]
. For example, being identity the identity function defined in Scala,Option(identity(1)) == identity(Option(1))
- Morphism composition: If we have two morphisms f: A => B and g: B => C, it must be checked that F[f o g] = F[f] o F[g]. It’s not as complicated as it seems:
val f: Int => String = _.toString val g: String => Boolean = _.length > 1 val l: List[Int] = List(1,20,3) l.map(f).map(g) == l.map(f andThen g) == l.map(n => g(f(n))) //List(false, true, false)
Even though, we can describe functors (in the context of a F[_] production chain) as the instructions for transforming some given A input value into some B output value within the same F[_] production chain, by using a morphism (a transformation function) A => B.
Functor types
Ordinary functors are also known as co-variant functors (in order to differentiate itself from contravariant and invariant functors). Let’s se what makes the difference between these different kinds of functor:
Contravariant Functor
Formal definition says that a F[_] functor is contravariant if, instead of having the map
method, it has a contramap
method defined:
trait Contravariant[F[_]]{ def contramap[A, B](fA: F[A])(f: B => A): F[B] }
That’s it: if it exists a function B => A, the contramap method defines F[A] => F[B].
…ok, let’s stop being so hardcore. We’ll illustrate it with an example.
Imagine a type class that knows how to compare certain type elements:
type Comparison = Int val Greater = 1 val Equal = 0 val Lower = -1 trait Comparator[T]{ def compare(t1: T, t2: T): Comparison }
Imagine as well, that I know how to compare integer numbers (and here comes the tricky tip):
if I know how to compare integers and I know how to convert ‘cucumbers’ into integer numbers, I do know how to compare ‘cucumbers’
object ComparatorF extends Contravariant[Comparator]{ def contramap[A, B] (fA: Comparator[A]) (f: B => A): Comparator[B] = new Comparator[B]{ def compare(t1: B, t2: B): Comparison = fA.compare(f(t1), f(t2)) } }
And now, the so-expected example about how to generate a contravariant functor for cucumbers:
trait Cucumber val intC: Comparator[Int] = ??? val cucumberToInt: Cucumber => Int = ??? val cucumberC: Comparator[Cucumber] = ComparatorF.contramap(intC)(cucumberToInt) cucumberC.compare(new Cucumber{}, new Cucumber{})
…sublime…
Invariant functors
Invariant functors for F[_] are determined by the imap
method:
trait Invariant[F[_]] { def imap[A, B](fA: F[A])(f: A => B)(g: B => A): F[B] }
…once again the doubt is killing you: I know and I beg you for some time to explain properly. Let’s see some example.
Imagine a type class that knows how to store stuff in some database (MongoDB, for example):
case class ObjectId(hash: String) trait DataStorage[T]{ def store(t: T): ObjectId def get(id: ObjectId): T }
If we forget about possible effects (exceptions, timeouts and so) for not messing up the example, we could define an invariant functor for DataStorage that allows storing any-kind elements:
object DataStorageF extends Invariant[DataStorage]{ def invariant[A, B] (fA: DataStorage[A]) (f: A => B) (g: B => A): DataStorage[B] = { new DataStorage[B]{ def store(b: B): Option[ObjectId] = fA.store(g(b)) def get(id: ObjectId): B = f(fA.get(id)) } } }
So…
If I know how to store integers and I know how to convert integers into cucumbers (and the other way round), I know how to store cucumbers!
val intDS: DataStorage[Int] = ??? val cucumberToInt: Cucumber => Int = ??? val intToCucumber: Int => Cucumber = ??? val cucumberDS: DataStorage[Cucumber] = DataStorageF .imap(intDS)(intToCucumber)(cucumberToInt) val id = cucumberDS.store(new Cucumber{}) val originalCucumber = cucumberDS.get(id)
…
We couldn’t say this is all, but it may be a good introduction to the wonderful functor world. See you in the next poxt. ‘Like’ and share if you like storing cucumbers.
Peace out!
Sources:
[1] Advanced Scala with Cats – Noel Welsh & Dave Gurnell
[2] Variance and functors – Typelevel blog