Using Cat Data Reader Monad
Introduction
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.