Introduction
A Reader monad deals with unary functions: it wraps a single unary function and allows us to combine unary functions in two different ways.
The map method combines unary functions with types
A ⇒ B and B ⇒ C. The first function is the function wrapped by
the Reader, the second function is the client function
(the function supplied to map).
The flatMap method alows us to combine two wrapped functions of the same
input type: A ⇒ B and A ⇒ C, but of course it cannot be achieved directly.
The client function passed to flatMap accepts type B (the wrapped function’s
result type) and returns a Reader wrapping a function of type A ⇒ C.
| Scala standard library already includes a mechanism for combining unary functions: Function1 trait provides andThen and compose methods. |
Let’s first explore chaining of functions by using just the Scala standard library, without Reader monad.
Chaining functions: andThen
The method andThen can combine two unary functions f and g if the result type of f matches the argument type of g. The first function’s result is passed to the second function:
val duplicate: String => String = _ * 2
val countA: String => Int = _.count(_ == 'a')
val combined = duplicate andThen countA
combined("Abracadabra") // Result: 8
In other words:
| A function of type A ⇒ B chained with B ⇒ C gives a function of type A ⇒ C. |
Chaining functions: same input type
Can we somehow combine the functions of types A ⇒ B and A ⇒ C?
Obviously, we cannot combine them directly by using andThen.
Furthermore, what’s the purpose of combining if we just ignore
the first function’s result?
Let’s reformulate the question: can we somehow combine the functions
of types A ⇒ B and B ⇒ (A ⇒ C) to produce a function of
type A ⇒ C?
Here we are using the first function’s result B as well as the input
type A. Depending of our viewpoint the second function is a unary
function of argument A or a binary function of arguments B and A.
Of course we can do it:
def createAC[A, B, C] (ab: A => B, bac: B => (A => C)): A => C =
{
a: A =>
val aac: A => (A => C) = ab andThen bac
val ac: A => C = aac(a)
ac(a)
}
Let’s summarize it:
| A function of type A ⇒ B chained with B ⇒ (A ⇒ C) gives a function of type A ⇒ C. |
Cats Reader monad
The Scala Cats Reader monad is just a specialized Kleisli type:
type Reader[A, B] = Kleisli[Id, A, B]
However, the library provides a Reader object with the factory method apply:
def apply[A, B](f: (A) ⇒ B): Reader[A, B]
We can create Reader / Kleisli instances in this way:
val duplicateR = Reader[String, String](_ * 2)
and run it in either of following two ways:
duplicateR("hello")
duplicateR.run("hello")
| The wrapped function value is named run. |
pure
The pure method creates a wrapped function which ignores the input argument and always returns the same value.
The Reader object doesn’t provide a pure method, but we can achieve the same effect by calling Kleisli's pure:
import cats.data.Kleisli
import cats.implicits._
import cats.Id
val constant: Kleisli[Id, String, Int] = Kleisli.pure(456)
constant("abc") // Result: 456
ask
The ask method creates a wrapped identity function for a given type.
Since Reader object doesn’t provide ask method, we can just use Kleisli's ask:
import cats.data.Kleisli
import cats.implicits._
import cats.Id
val identityR: Kleisli[Id, String, String] = Kleisli.ask
identityR("abc") // Result: abc
Perhaps it is easier to wrap identity directly into a Reader instead of using ask:
val identityR = Reader[String, String](identity)
map
The Reader monad’s map method provides similar functionality as Function1's andThen: combining two unary functions where the result type of the first function and the argument type of the second function match.
Its conceptual signature (pretending for a moment that Reader was not a specialized Kleisli) looks like this:
def map[C](f: (B) ⇒ C): Reader[A, C]
The resulting function is simply:
run andThen f
Let’s rewrite a previous simple example using Reader and map:
import cats.data.Reader
val duplicate: Reader[String, String] = Reader(_ * 2)
val combined: Reader[String, Int] = duplicate map (_.count(_ == 'a'))
combined("Abracadabra") // Result: 8
andThen
This methods comes in two flavors:
def andThen[C](f: (B) ⇒ C): Reader[A, C]
def andThen[C](r: Reader[B, C]): Reader[A, C]
The first version is the same as map. The second version allows us to chain two Readers.
local
The method local has the following conceptual signature:
def local[A0](f: (A0) ⇒ A): Reader[A0, B]
It is similar to map and andThen, but the client function f comes first in the chain, not the second.
The resulting function is:
f andThen run
The following simple example illustrates this method:
import cats.data.Reader
val duplicateR = Reader[String, String](_ * 2)
val duplicateIntR = duplicateR.local[Int](_.toString)
duplicateIntR(123) // Result: 123123
flatMap
As we explained above, the flatMap method gives us enhanced functionality: it allows us to combine two Readers which wrap the functions with same input types:
def flatMap[C](f: (B) ⇒ Reader[A, C]): Reader[A, C]
Let’s illustrate it on an example from the book Scala with Cats.
import cats.data.Reader
case class Cat(name: String, favoriteFood: String)
val garfield = Cat("Garfield", "lasagne")
val greetR: Reader[Cat, String] = Reader(cat => s"Hello ${cat.name}")
val feedR: Reader[Cat, String] = Reader(cat => s"Have a nice bowl of ${cat.favoriteFood}")
val greetFeedR: Reader[Cat, String] = greetR.flatMap { greet =>
feedR.map { feed => s"$greet. $feed" }
}
println( greetFeedR(garfield) )
// Result: Hello Garfield. Have a nice bowl of lasagne.
Syntactic sugar
As always, combining monads is easier if we use for-comprehensions:
val greetFeedR2: Reader[Cat, String] = for {
greet <- greetR
feed <- feedR
} yield s"$greet. $feed"
Alternatives
Plain functions
Actually, the above example is not very impressive since we can very easily achieve the same effect by using just plain functions, even without andThen composition:
val greetR: Cat => String = cat => s"Hello ${cat.name}"
val feedR: Cat => String = cat => s"Have a nice bowl of ${cat.favoriteFood}"
val greetFeedR3: Cat => String = { cat =>
val greet = greetR(cat)
val feed = feedR(cat)
s"$greet. $feed."
}
Methods
Furthermore, the question is: why should we write the functions operating on the Cat class outside of the class, when a significantly simpler way is just to write the methods instead.
case class Cat(name: String, favoriteFood: String)
{
def greet = s"Hello $name"
def feed = s"Have a nice bowl of $favoriteFood"
def greetFeed = s"$greet. $feed"
}
Implementation
It can be instructive to implement a simple version of some monad or other entity we are trying to understand, and I think it is true in this case as well:
case class Reader[A, B] (val run: A => B) {
def map[C](f: B => C): Reader[A, C] = { Reader(run andThen f) }
def flatMap[C](f: B => Reader[A, C]): Reader[A, C] = {
val aToC: A => C = { a: A =>
val combinedFn: A => Reader[A, C] = run andThen f
val readerAC: Reader[A, C] = combinedFn(a)
readerAC.run(a)
}
Reader(aToC)
}
}
object Reader {
def pure[A, B](b: B): Reader[A, B] = {
val aToB: A => B = { a: A => b }
Reader(aToB)
}
}
This implementation along with the usage examples is available here.
Example
The following example was taken from an article and converted from Haskell to Scala. The example is interesting since it is non-trivial and it has a purpose: to avoid passing the same argument around.
The example generates web content (HTML code) for the following hierarchy:
-
view
-
page
-
topnav
-
content (needs email)
-
left
-
right
-
article
-
widget (needs email)
-
-
-
-
-
Each of the above entities is implemented by a function returning HTML. Several functions need an argument (email), which means that all other functions need this argument, too.
To avoid passing the same argument from function to function down the hierarchy, the example employs Reader monad: each function returns a Reader.
import cats.data.Reader
type Email = String
type Html = String
def div(children: List[Html]): Html = "<div>" + children.mkString + "</div>"
def h1(children: List[Html]): Html = "<h1>" + children.mkString + "</h1>"
def p(children: List[Html]): Html = "<p>" + children.mkString + "</p>"
def identityR = Reader[Email, Html](identity)
def viewR: Reader[Email, Html] = for {
page <- pageR
} yield div(List(page))
def pageR: Reader[Email, Html] = for {
content <- contentR
} yield div(List(topNav, content))
def topNav: Html = div (List(h1(List("Our site"))))
def contentR: Reader[Email, Html] =
for {
email <- identityR
right <- rightR
} yield div(
List(
h1(
List("Custom Content for " + email)
),
left,
right
)
)
def left: Html = div (List(p(List("This is the left side"))))
def rightR: Reader[Email, Html] =
for {
article <- articleR
} yield div(
List(article)
)
def articleR: Reader[Email, Html] =
for {
widget <- widgetR
} yield div(
List(
p(List("This is an article")),
widget
)
)
def widgetR: Reader[Email, Html] =
for {
email <- identityR
} yield div(List(p(List("Hey " + email + " we've got a great offer for you!"))))
println(
viewR("john@coolmail.com")
)
Alternatives
The goal of avoiding the need to pass an argument around can be achieved in other ways as well. Let’s explore the alternatives.
Implicit arguments
Scala provides implicit arguments whose purpose is exactly to avoid explicit passing of boilerplate arguments. In this example each method just takes an implicit email argument which is not explicitly passed to lower-level functions:
...
def content (implicit email: Email): Html =
div(List(
h1(
List("Custom Content for " + email)
),
left,
right
)
)
...
def right (implicit email: Email): Html = div(List(article))
def article (implicit email: Email): Html = div(
List(
p(List("This is an article")),
widget
)
)
...
Nested methods
We can avoid passing the email parameter around by providing the argument only in the top method which embraces all lower-level methods. Other methods don’t need to declare the argument at all:
def view (email: Email): Html =
{
def page: Html = div(List(topNav, content))
def topNav: Html = div (List(h1(List("Our site"))))
...
div(List(page))
}
The full example is available here.
Class
Finally, a simple class where email is a constructor parameter will also do the job. All methods have access to the parameter without declaring it:
class WebPage (email: Email)
{
def view: Html = div(List(page))
def page: Html = div(List(topNav, content))
def topNav: Html = div (List(h1(List("Our site"))))
...
}
Evaluation
Each of the presented alternatives has about 40 lines of code, significantly smaller compared to about 60 lines of Reader example. Furthermore, each alternative example is much easier to read and understand.
| In my view, using Reader monad to avoid passing an argument around doesn’t provide benefits (at least not in Scala). There are simpler alternatives. |