Scala Cats library for dummies- part 4

This article will center on cats library monad type class, but before we proceed, it will be nice to have a good understanding of the flatMap function in the standard scala collections. FlatMap is basically two functions in one i.e map and flatten. As an example, say we want to multiply the elements of two(2) Lists of integers and return a single list, one approach is to use the map function of the first List to iterate over the second List then combine(flatten) the final result.

scala> val first = List(2, 5)
first: List[Int] = List(2, 5)
scala> val second = List(4, 8)
second: List[Int] = List(4, 8)
scala> first.map( e1 => second.map(e2 => e1 * e2))
res0: List[List[Int]] = List(List(8, 16), List(20, 40))
scala> res0.flatten 
res1: List[Int] = List(8, 16, 20, 40)


Notice how we used flatten to merge List[List[Int]] into List[Int]. Flatten can also be used for nested option types. lets see a very trivial example.

scala> val a = Option(5)
a: Option[Int] = Some(5)
scala> def intToSome(x:Int) = Some(x) 
intToSome: (x: Int)Some[Int]
scala> a.map( o => intToSome(o))
res5: Option[Some[Int]] = Some(Some(5))
scala> res5.flatten
res6: Option[Int] = Some(5)
scala> val a = Option(5)
a: Option[Int] = Some(5)
scala> def intToSome(x:Int) = Some(x)
intToSome: (x: Int)Some[Int]
scala> a.flatMap( x => intToSome(x))
res0: Option[Int] = Some(5)
scala> first.flatMap( f => second.map(s => f * s))
res2: List[Int] = List(8, 16, 20, 40)

In both examples flatten function was used to merge the final result into a single data type. The flatten function takes a nested context F[F[A]] to return F[A]. We can however use the flatMap function in the standard library to achieve the same computation when ever we find ourselves writing map and flatten expressions.

F[F[A]]             => F[A]
Option[Option[Int]] => Option[Int]
List[List[Int]] => List[Int]

Below is the signature of the flatMap function

def flatMap[B](f: A => F[B]): F[B]

Remember, we discussed Applicative pure function back then in part 3 , now, adding flatMap to the Applicative type class gives you a Monad( any instance that have the pure and flatMap methods).

We have already seen flatMap in action in our previous List and Option examples above, however, monads are in almost everywhere in all scala collections and data types(Set, Vector, Array, Map , Future* etc ), even though there is no concrete class or trait called monad in the standard scala language.

Using Monads

Lets have a look at an example of monad usage : say we have a student school management application and we want to implement a module that will compute the individual performance of a student for a particular session. First, we need to retrieve the student profile information, then retrieve all the courses he/she registered for that session, and finally retrieve all the grades of these courses.

case class Grade(id:Int, studentId:Int, grade: String)
case class Student(id: Int, name: String)
case class StudentCourse(id:Int,
studentId: Int,
courseId: Int)
object StudentService{
def getStudentById(id: Int): Option[Student] = {}
}
object StudentCoursesService{
def getCourses(student: Student): Option[List[StudentCourse]]={}
}
object StudentGrades{
def getGrades(lc: List[StudentCourse]): Option[List[Grade]]={}
}

Looking carefully at the return types of the service methods(options), we can simply flatmap everything.

val studentGrade = StudentService.getStudentById(id)
.flatMap(getCourses)
.flatMap(getGrades)

Alternatively, we can use the for-comprehension which is a composition of map and flatMap.

val studentGrade = for {
student <- getStudentById(id)
courses <- getCourses(student)
grades <- getGrades(courses)
}yield grades

if we had to do this without using monads we will having a lot of if/then/else statement or nested loops flying everywhere (where fixing a bug could possibly give you two free bugs :{ ).

Libraries that are fully non-blocking with asynchronous I/O operation usually encapsulated their return types in types like futures , one example of such libraries is reactivemongo. When using such libraries you will likely have to use thefuture ‘s flatmap method in composing multiple operations together.

Monads in Cats

Essentially,flatMap is the most fundamental function of Monad and authors of cats library also built its monad based on this principle — by using flatMap and pure to implement flatten and map, however, they also added another requirement to all cats library monad implementation, called tailRecM ( Tail recursive Monad), the reason behind this additional requirement is to make monadic recursion stack safe( since monadic recursion are quite common and a poor implementation could lead to stack overflow on the jvm ).

Below is a Monad implementation for Option.

import scala.annotation.tailrec

implicit val optionMonad = new Monad[Option] {
def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f)
def pure[A](a: A): Option[A] = Some(a)

@tailrec
def tailRecM[A, B](a: A)(f: A => Option[Either[A, B]]): Option[B] = f(a) match {
case None => None
case Some(Left(nextA)) => tailRecM(nextA)(f) // continue the recursion
case Some(Right(b)) => Some(b) // recursion done
}
}

In conclusion, I deliberately did not discuss that monad laws as what we have learnt is enough for you kick start your journey to the land of monads! However, Monads is not everything, but having it in your tool box is definitely a thing to work in the functional world of scala.

Lets take a break from here, but don't hesitate to buzz me if you don't understand any aspect of this writeup. Our next discuss will be on the OptionT which is also a monad.

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

Big thanks to @edmundnoble and @TheAlmikey_twitter for reviewing this article.