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 future
s , 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.