Saturday, 9 April 2016

The Reader Monad

Yet another exploration in Monadland. Like the State Monad, its sibling, Read Monad had managed to elude me until I came across an enlightening example in Debashish Gosh's excellent book, Functional and Reactive Domain Modelling. In the following example I'll describe a simple scenario where I'd usually use dependency injection and refactor it to a Reader monad using variant.

Version 1. Dependency Injection

case class User(id: Int)
trait NotificationService {
def notifyUserAboutSth(user: User): Unit
}
trait UserRepository {
def find(id: Int): Option[User]
}
class UserService(
repository: UserRepository,
notificationService: NotificationService) {
def notifyUser(userId: Int): Unit = repository.find(userId) match {
case Some(foundUser) ⇒ notificationService.notifyUserAboutSth(foundUser)
case None ⇒ println("No user found!")
}
}
view raw DIExample.scala hosted with ❤ by GitHub
When one tries to follow FP principles, she strives to build her application up like an onion. The core should contain pure functions, and all interaction with external services - DB, web service calls, user input, ... - , i.e. side-effects, should be confined to the outer layer. In the code above the domain logic and side-effects are undisentanglable. The next version shows an alternative.

Version 2. Higher order function

case class Context(userRepository: UserRepository, notificationService: NotificationService)
object UserService {
def notifyUser(userId: Int): Context ⇒ Unit = context ⇒
context.userRepository.find(userId) match {
case Some(foundUser) ⇒ context.notificationService.notifyUserAboutSth(foundUser)
case None ⇒ println("No user found!")
}
}
//build up the computation
val notify: Context => Unit = UserService.notifyUser(joe)
//fire the side-effects
notify(context)
This is better. `notifyUser` is now a referentially transparent function. The actual execution of the effects is deferred to a later point, when the result function is called with the context. The Reader monad is nothing else just a convenient wrapper around such a function.

Version 3. Reader Monad

case class Reader[R, A](run: R ⇒ A) {
def map[B](f: A ⇒ B): Reader[R, B] = Reader(r ⇒ f(run(r)))
def flatMap[B](f: A ⇒ Reader[R, B]): Reader[R, B] = Reader(r ⇒ f(run(r)).run(r))
}
object UserService {
def notifyUser(user: User): Reader[Context, Unit] = Reader { context ⇒
context.userRepository.find(user.id) match {
case Some(foundUser) ⇒ context.notificationService.notifyUserAboutSth(foundUser)
case None ⇒ println("No user found!")
}
}
}
//build up the computation
val notify: Reader[Context, Unit] = UserService.notifyUser(joe)
//fire the side-effects
notify.run(context)
The benefit Reader monad offers over the simple HOF-solution is the monadic composability, like in the example below.

trait AccountService {
def credit(userId: Int, amount: Int): Reader[Context, Unit]
def debit(userId: Int, amount: Int): Reader[Context, Unit]
}
val service: AccountService = ???
val transfer: Reader[Context, Unit] = for {
_ ← service.credit(1, 10)
_ ← service.debit(2, 10)
} yield ()
//fire the side-effects
transfer.run(context)

Note that inside the for comprehension the context doesn't even appear.

No comments :

Post a Comment