Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the benefits of Reader monad?

I have read a blog post about Reader monad.

The post is truly great and explains the topic in details but I did not get why I should use the Reader monad in that case.

The post says: Suppose there is a function query: String => Connection => ResultSet

def query(sql:String) = conn:Connection => conn.createStatement.executeQuery(sql)

We can run a few queries as follows:

def doSomeQueries(conn: Connection) = {
  val rs1 = query("SELECT COUNT(*) FROM Foo")(conn)
  val rs2 = query("SELECT COUNT(*) FROM Bar")(conn)
  rs1.getInt(1) + rs2.getInt(1)
} 

So far so good, but the post suggests use the Reader monad instead:

class Reader[E, A](run: E => A) {

  def map[B](f: A => B):Reader[E, B] =
    new Reader(е=> f(run(е)))

  def flatMap[B](f:A => Reader[E, B]): Reader[E, B] =
    new Reader(е => f(run(е)).run(е))  
}

val query(sql:String): Reader[Connection, ResultSet] =
  new Reader(conn => conn.createStatement.executeQuery(sql))

def doSomeQueries(conn: Connection) = for {
  rs1 <- query("SELECT COUNT(*) FROM Foo")
  rs2 <- query("SELECT COUNT(*) FROM Bar")
} yield rs1.getInt(1) + rs2.getInt(1)

Ok, I got that I don't need to thread connection through the calls explicitly. So what ?
Why the solution with Reader monad is better than the previous one ?

UPDATE: Fixed the typo in def query: = should be => This comment only exists because SO insists that edits must be at least 6 chars long. So here we go.

like image 833
Michael Avatar asked Mar 08 '14 15:03

Michael


People also ask

Why is Monad useful?

monads are used to address the more general problem of computations (involving state, input/output, backtracking, ...) returning values: they do not solve any input/output-problems directly but rather provide an elegant and flexible abstraction of many solutions to related problems.

What is the reader Monad?

The Reader monad (also called the Environment monad). Represents a computation, which can read values from a shared environment, pass values from function to function, and execute sub-computations in a modified environment. Using Reader monad for such computations is often clearer and easier than using the State monad.

What is state Monad?

The state monad is a built in monad in Haskell that allows for chaining of a state variable (which may be arbitrarily complex) through a series of function calls, to simulate stateful code.


2 Answers

The most important reason is that the reader monad allows you to build up complex computations compositionally. Consider the following line from your non-reader example:

val rs1 = query("SELECT COUNT(*) FROM Foo")(conn)

The fact that we're passing in conn manually means that this line doesn't really make sense on its own—it can only be understood and reasoned about in the context of the doSomeQueries method that gives us conn.

Often this is just fine—there's obviously nothing wrong about defining and using local variables (at least in the val sense). Sometimes, though, it's more convenient (or desirable for other reasons) to build up computations out of stand-alone, composable pieces, and the reader monad can help with this.

Consider query("SELECT COUNT(*) FROM Foo") in your second example. Assuming we know what query is, this is an entirely self-contained expression—there are no variables like conn that need to be bound by some enclosing scope. This means you can reuse and refactor more confidently, and that you don't have quite so much stuff to hold in your head when you're reasoning about it.

Again, this isn't ever necessary—it's largely a matter of style. If you decide to give it a try (and I'd suggest that you do), you'll probably pretty quickly develop preferences and intuitions about where it makes your code more intelligible and where it doesn't.

One other advantage is that you can compose different kinds of "effects" using ReaderT (or by adding Reader into some other stack). That set of issues probably deserves its own question and answer, though.

One last note: you probably want your doSomeQueries to look like this:

def doSomeQueries: Reader[Connection, Int] = for {
  rs1 <- query("SELECT COUNT(*) FROM Foo")
  rs2 <- query("SELECT COUNT(*) FROM Bar")
} yield rs1.getInt(1) + rs2.getInt(1)

Or, if this really is the end of the line:

def doSomeQueries(conn: Connection) = (
  for {
    rs1 <- query("SELECT COUNT(*) FROM Foo")
    rs2 <- query("SELECT COUNT(*) FROM Bar")
  } yield rs1.getInt(1) + rs2.getInt(1)
).run(conn)

In your current version you're not actually using conn.

like image 189
Travis Brown Avatar answered Oct 18 '22 09:10

Travis Brown


For finding out the general benefits of using ReaderMonad I recommend Travis Brown's excellent answer - the strength of ReaderMonad lies in its compositionality and other extras provided by monads (e.g. the ReaderT et al). You get the most benefit out of it if you write your other code in monadic style too.

You've also asked specifically what's so desirable in not having to pass the connection around explicitly. I'll try to answer this part of your question here.

First, few words less to type / less to read is already an improvement. The more complex the whole codebase is the more I appreciate that. When I read a long method (not written by me of course ;) ) I find it easier when its logic isn't interwoven with dumb argument passing.

Second, ReaderMonad gives you a guarantee, that the connection is the same object all the way down. Most often you want exactly that. In your first example it's very easy to call

query("SELECT COUNT(*) FROM Bar")(anotherConnectionWhereverItCameFrom)

regardless of whether it's been done for purpose or by mistake. When I read a long method and see ReaderMonad used I know that there'll be only one connection used. No nasty surprises caused by some "tactical solution" in the 219th line of the method.

Note, that those benefits can be also achieved without ReaderMonad, even if it does good job in that area. You could for example just write:

class Query(val connection: Connection) {
  def apply(sql:String) = connection.createStatement.executeQuery(sql)
}

def doSomeQueries(query: Query) = {
  val rs1 = query("SELECT COUNT(*) FROM Foo")
  val rs2 = query("SELECT COUNT(*) FROM Bar")
  rs1.getInt(1) + rs2.getInt(1)
}
doSomeQueries(new Query(connection))

It wouldn't have neither composability nor other nice features of monads, but would achieve the ReaderMonad's goal of not passing the argument (connection) explicitly.

like image 2
Przemek Pokrywka Avatar answered Oct 18 '22 10:10

Przemek Pokrywka