Scala Cats library for dummies — part 3

Welcome back to our series of cats library type classes. In this write up we’ll be looking at “applicative” , “semigroup” and “monoid” type classes.

Applicative

Firstly, the applicative type class is a descendant of the Apply type class we discussed briefly in our previous article, it basically comes with a function called “pure” which wraps a data type(Int, string, Boolean) into a context (Option, List , Future ). In other terms, it takes a single data type A and return a data type F[A](lifts any value into the applicative functor).

Below is the signature of the “pure” function.

def pure[A](x: A): F[A]

Applicative demo

scala> import cats._ , cats.implicits._ , cats.instances._
import cats._
import cats.implicits._
import cats.instances._
scala> Applicative[Option].pure(1)
res0: Option[Int] = Some(1)
scala> Applicative[List].pure(4)
res1: List[Int] = List(4)

Semigroup

A semigroup is an algebraic structure consisting of a set together with an associative binary operation.

The cats library semigroup implementation comes with a function “combine” which simply combines 2 values of the same data type(Int, List , string, Option, Map).

Associativity is the only principle(law) that the semigroup type class follows. 
- (a x b) x c == a x (b x c) == (b x c) x a

When we add or multiply a list of variables regardless of their order of arrangement yields the same result.

Imagine the semigroup combine function like a git merge with on conflicts :)

scala> import cats._ , cats.instances._ , cats.implicits._
import cats._
import cats.instances._
import cats.implicits._
scala> 2.combine(3)
res0: Int = 5
scala> List(1,3,5).combine(List(1,25,6))
res1: List[Int] = List(1, 3, 5, 1, 25, 6)
scala> Option(4).combine(Option(6))
res2: Option[Int] = Some(10)
scala> Option(2).combine(None)
res3: Option[Int] = Some(2)

Remember that writing an express like “2.combine(3)” is possible because we have the implicits functions in scope “cats.implicits._” — cats library have provided a lot of primitive data type implicits readily available for use.

Semigroup also has a symbolic operator |+| which ease typing, I personally like this because its kinda of cleaner!

scala> 2 |+| 5
res4: Int = 7
scala> List(1,2,4) |+| List(3,5,6)
res5: List[Int] = List(1, 2, 4, 3, 5, 6)
scala> Map("a"-> 1 ) |+| Map("b"-> 2)
res6: Map[String,Int] = Map(b -> 2, a -> 1)

Monoid!

Hummmmm……. yet another scary word but is a great tool that can efficiently parallelize computations.

So what is a monoid ?

A monoid is an algebraic structure that follows these two basic rules(laws).

1. A binary associative operation that takes two values of type A and combines them into one.
2. A value of type A that is an identity for that operation.

Lets do a simple arithmetics, when adding 2 integers 3 + 7 we get 10, however, when we add zero(0) to any integer the result will be the same regardless of whether the value zero(0) comes in-front or behind, same applies to multiplication, multiplying an integer with one(1) returns the same result. So therefore we can call zero(0) an identity element of addition and one(1) an identity element for multiplication.

With the definition above we can implement the monoid signature.

trait Monoid[A]{
def combine(x: A , y: A): A
def empty: A
}
#for int addition the monoid will look this
val intAddition = new Monoid[Int]{
def combine(x:Int, y:Int) = x + y
def empty = 0
}
#for int multiplication we have 
val intMultiplication = new Monoid[Int]{
def combine(x:Int, y:Int) = x*y
def empty = 1
}
#for string concatenation we have 
val stringMonoid = new Monoid[String]{
def combine(x:String, y:String) = x + y
def empty = ""
}

As an exercise you should implement monoid for List, booleanOr and booleanAnd and Options.

But why monoids ? What do we stand to benefit from monoids ? Well, it turns out that we can write very interesting programs over any data type, knowing nothing about that type other than that it’s a monoid.

Secondly, the fact that a monoid’s operation is associative means that we have a great deal of flexibility in how we fold a data structure like a list, this means that we can split our data into chunks, fold them in parallel , and then combine the chunks with the monoid.

In the standard scala collections(Sequence, Vectors, Lists , Ranges etc) there exist these methods fold, foldLeft, foldRight — which are used to aggregate values together to return a single value, these methods takes monoids as input parameters.

Lets try out adding a list of integers using the intAddition monoid we defined.

scala> import intAddition._
import intAddition._
scala> List(1,2,4,5).fold(empty)(combine)
res1: Int = 12
scala> import intMultiplication._
import intMultiplication._
scala> List(1,2,4,5).fold(empty)(combine)
res2: Int = 40

Back to cats implementation of monoid

when you add an “empty” method(which is equivalent to identity element) to the semigroup combine method we discussed above, you simply get our monoid.(hummm, pretty simple!)

Also, we can find out ourselves that cats library already have predefined the “empty” method for primitive types.

scala> Monoid[Int].empty
res0: Int = 0
scala> Monoid[Float].empty
res1: Float = 0.0
scala> Monoid[String].empty
res3: String = ""

As stated in the cats documentation - the advantage of using these type class provided methods, rather than the specific ones for each type, is that we can compose monoids to allow us to operate on more complex types.

Monoid[Map[String,Int]].combineAll(List(Map("a" -> 1, "b" -> 2), Map("a" -> 4)))
res0: Map[String,Int] = Map(b -> 2, a -> 5)

Lets take a break from here, our next discuss will be on the Monad type class. See you then.

If you like this writeup, click the💚 below so more people can see it here on Medium.

Big thanks to some catlords @fabianmeyer , @mgttlinger and @tkroman for reviewing this article.