Using Cat Data Reader Monad

Introduction

Ayache Khettar
2 min readJul 2, 2017

In this short post, I would like to introduce you the Reader monad — see cats documentation

The first thing you need to know is that the Reader monad represents a function: A => B, i.e:

class Reader[A,B](run: A => B) {
// details omitted
}

We could use the Reader monad to achieve various things, but I would like to show you in this post how to use it to achieve composition and dependency injection.

Composition

Using the above class definition of the Reader we can create tow instances of a Reader with matching type.

import cats.data.Reader val upper = Reader((text: String) => text.toUpperCase)
val greet = Reader((name: String) => s"Hello $name")

We have two Readers with matching type, we can combine them as we like to produce a new Reader without the risk of producing side effect.

val comb1 = upper.compose(greet)
val comb2 = upper.andThen(greet)
val result = comb1.run("Bob")
println(result) // prints Hello Bob

Dependency Injection

Consider the two services below. The user can only register in a course if he is authorised. So the CourseService is dependent on the result of the AuthService

case class Course(desc: String, code: String)

class AuthService {
def isAuthorised(userName: String): Boolean = userName.startsWith("J")
}

class CourseService {
def register(course: Course,
isAuthorised:
Boolean,
name: String) = {
if (isAuthorised)
s"User $name registered for the course: ${course.code}"
else
s"User: $name is not authorised to register for course: ${course.code}"
}
}

Let’s create a class which represent the current environment and call it CourseManager.

case class CourseManager(course: Course,
userName: String,
authService: AuthService,
courseService: CourseService)

When the application is run the CourseManager will have all the services needed to carry out the business transaction.

Let’s create the Reader monad which allows us to write business methods not tied to the services

def isAuthorised = Reader[CourseManager, Boolean]{ courseMgr =>
courseMgr.authService.isAuthorised(courseMgr.userName)
}

def register(isFull: Boolean) = Reader[CourseManager, String] { courseMgr =>
courseMgr.courseService.register(courseMgr.course,
isFull,
courseMgr.userName)
}

Note that in both functions, the CourseManager shows up in the return type. Therefore we can combine these two functions using for comprehension as follow:

val result = for {
authorised <- isAuthorised
response <- register(authorised)
} yield response

The above result will not be executed until we run our application. Let’s run our application now:


val
course = Course("Computer Science", "CS01")
val report = result.run(CourseManager(course, "Jon", new AuthService, new CourseService))
println(report) // prints: User Jon registered for the course: CS01

I hope this short introduction will lead you find out about other features provided by the TypeLevel Libraries.

--

--