Cats

Cats is a simple and concise functional programming library for Scala. It appeals to me for its compactness and simpler class hierarchy.

I have no formal education in category theory. But I am a student of theoretical Mathematics and have studied Abstract Algebra. Though I am not aware of formal category theory terms, I will try converting them to simple set theory.

Category theory is concerned with study of algebraic structures. Algebraic system can be described as a set of objects together with some operations for combining them. Example’s can be vectors, groups, fields etc. Or even Sets.

The aim of this blog post is to explain cats first in a very simple lucid way. And they try putting it in code. I will be using some terms:

  1. Universe: For example: Integer-Universe would imply a universe containing integers or set of integers. Think of it as our infinite terrestrial universe which contains milky way which contains solar system and ultimately planets.
  2. Space/Type: A set of elements corresponding to a property. Space exists in universe. You can think Space as a type in scala.

Functor

Let A and B be space in some universe. And there exists a function `f` from A to B.  Then Functor for `X` universe is a mapping from (mapping of space A in X universe) `X(A)` to (mapping of space `B` in universe `X`) `X(B)`

Factor

Now what does this mean in scala world? Say we have a function `f` that tells us how to convert from type A to type B. So `f` does `Int => String`.

Now we want a mechanism to convert elements of another universe, say `Option` universe. First we need a mechanism to convert from space `A` in our universe to `Option(A)` (Space A in Option universe). We will see how it happens later below (Applicative).

So now some how we have mapped A to Option universe. Lets call it `Option(A)` space. We wish to convert from space `Option(A)` to space `Option(B)` (i.e. we need to `Option[Int] => Option[String]`) (Or F[Option] in above image)

Functors are exactly for this purpose with `map` being its flagship method:

trait Functor[F[_]]  { self =>
    def map[A, B](f: F[A])(f: A => B): F[B]
}
implicit val OptionFunctor = new Functor[Option] {
  override def map[A, B](fa: Option[A])(f: (A) => B): Option[B] = fa map f
}

Functor[Option].map(Some(1))(x => x*13)
Functor[Option].map(Some(1))(x => x.toString)

Functor[Option] is a mapping mechanism from space `Option(A)` to set `Option(B)` in Option universe. Think of it as:

We know how to convert from a space A to another space B in some universe. In another universe X, Functor of X lets us convert from X(A) to X(B)

So it lets you convert from `Option[Int] => Option[String]` or `Option[String] => Option[List[Int]]` etc. In short any space to any space in `Option` universe

Lift:

Lift is my favorite method in Functor. Notice with map, provided you have `Option[A]`, map converts to `Option[B]`. i.e. it consumes `Option[A]` object. But the conversion from `Option[A] => Option[B]` is just another function can be useful if it can be pass around as function. i.e. `F[X]` in above image.

def toStri(n:Int) = n.toString
val lift: (Option[Int]) => Option[String] = Functor[Option].lift(toStri)
lift(Some(1))

If you think, lift seems something like this:

val lift = Functor[Option].map _ //ignore map types

`lift` seems everything of a `map` but as a pure function. And thats how lift is implemented

def lift[A, B](f: A => B): F[A] => F[B] = 
                                          x => map(x)(f)

For a function from Space A to Space B, lift provides a function in Universe X to convert from X(A) to X(B)

Applicative

Now notice, using a Functor we can convert from X(A) to X(B), given a function A to B. But how do we convert an element x belonging to A, to an element in X(A)?
Applicative
Applicative lets us do that:

implicit val OptionApplicative = new Applicative[Option] {
  override def pure[A](x: A): Option[A] = Option(x)

  override def apply[A, B](fa: Option[A])(f: Option[(A) => B]): Option[B] = ...
}
val op: Option[Int] = Applicative[Option].pure(1)

`pure` lets us convert an element from a universe to another. In above case, it lets us convert from element of type `Int` to `Option[Int]`. Applicative is an `Apply`

Apply

Apply is also a Functor.

def apply[A, B](fa: F[A])(f: F[A => B]): F[B]

Just instead of the function `f` which converts from `A => B`, you have `X(A => B)`. i.e. a function in X universe. Notice it is not `X[A] => X[B]`.


Lets have some more comprehensive example:

implicit val ListApplicative = new Applicative[List] {
  override def pure[A](x: A): List[A] = List(x)

  override def apply[A, B](fa: List[A])(f: List[(A) => B]): List[B] = fa.flatMap(x => f.map(y => y(x)))
}

val lsOp = Applicative[List].pure(2)

You ask the Applicative for `List` to convert an element 2 to something in `List` universe. i.e. `List(2)`

 
scala> val ls = Applicative[List].apply(List(1,2,3))(List(x => "a=>"+x, y => "b ==>"+y)) 
ls: List[String] = List(a=>1, b ==>1, a=>2, b ==>2, a=>3, b ==>3)

scala> val ls2 = Applicative[List].apply2(List(1,2,3), List(4,5,6))(List((x:Int,y:Int) => x+" --a--> "+y, (x:Int,y:Int) => x+" --b--> "+y))
ls2: List[String] = List(1 --a--> 4, 1 --b--> 4, 2 --a--> 4, 2 --b--> 4, 3 --a--> 4, 3 --b--> 4, 1 --a--> 5, 1 --b--> 5, 2 --a--> 5, 2 --b--> 5, 3 --a--> 5, 3 --b--> 5, 1 --a--> 6, 1 --b--> 6, 2 --a--> 6, 2 --b--> 6, 3 --a--> 6, 3 --b--> 6)

Compose:

implicit val OptionApplicative = new Applicative[Option] {
  override def pure[A](x: A): Option[A] = Option(x)

  override def apply[A, B](fa: Option[A])(f: Option[(A) => B]): Option[B] = fa.flatMap(x => f.map(y => y(x)))
}
scala> val comp= (Applicative[List] compose Applicative[Option])
comp: cats.Applicative[[α]List[Option[α]]] = cats.Applicative$$anon$1@168d1858

scala> comp.pure(12)
res5: List[Option[Int]] = List(Some(12))

Compose is quite interesting. So say you have an Applicative for List universe and another Applicative for Option universe. By composing them, you get an Applicative for `List[Option]`. Calling pure convert’s 12 from Int to a List(Option(1)).

PS: Will keep updating with time