Reputation: 4112
I am a scala newbie. I come from a background of Java. I have been reading up on monads and have formed a general idea about it. While i can appreciate the map
and flatMap
operations on types such as List
i cannot wrap my head around what they mean when it comes to reader monad
s.
Could someone please put up some simple examples of it.
I understand we need ReaderMonads to facilitate unary function composition so that we can use fancy syntax like for - comprehensions. I also understand that we need the monad gods satisfied in order for that to happen. All i want to understand is "What does map and flatmap" mean for functions ?
Upvotes: 1
Views: 541
Reputation: 7320
The reader monad, often written Reader[A, B]
, is just the function type A => B
. An encoding of monads in Scala looks like:
trait Monad[M[_]] {
def pure[A](a: A): M[A]
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}
where map
can be implemented in terms of pure
and flatMap
like so:
def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(a => pure(f(a)))
So first thing we need to do is to make our binary type constructor Reader
fit into the unary type constructor that Monad
expects. This is done by fixing the first (input) type parameter and leaving the second (output) type parameter free.
implicit def readerMonad[X]: Monad[X => ?] = ???
(The use of ?
here is via the wonderful kind-projector compiler plugin).
Starting with pure
, we substitue occurences of M[_]
with X => _
.
def pure[A](a: A): X => A
Given some value of type A
, we must return a function that given a value of type X
, returns a value of type A
. The only thing this can possibly be is the constant function.
def pure[A](a: A): X => A = _ => a
Now substituting flatMap
..
def flatMap[A, B](ma: X => A)(f: A => (X => B)): X => B = (x: X) => ???
This is a bit tricky, but we can use types to guide us to the implementation! We have:
ma: X => A
f: A => (X => B)
x: X
And we want to get a B
. The only way we know how to do that is via f
, which wants an A
and an X
. We have exactly one X
from x
, but we need an A
. We see we can only get an A
from ma
, which wants an X
, which again only x
provides us. Therefore we have..
def flatMap[A, B](ma: X => A)(f: A => (X => B)): X => B =
(x: X) => {
val a = ma(x)
val b = f(a)(x)
b
}
Read aloud, flatMap
on Reader
says to read some value A
out of the "environment" (or "config") X
. Then, branching on A
, read another value B
from the environment.
We can go ahead and implement map
manually as well:
def map[A, B](ma: X => A)(f: A => B): X => B = ???
Looking at the arguments X => A
and A => B
, with the expected output X => B
, this looks exactly like function composition, which indeed it is.
Example usage using cats with imports:
import cats.data.Reader
Let's say we have some Config
type:
case class Config(inDev: Boolean, devValue: Int, liveValue: Int)
which tells us whether or not we're in a "dev" environment and gives us values for something for "dev" and for "live." We can start by writing a simple Reader[Config, Boolean]
which read out the inDev
flag for us.
val inDev: Reader[Config, Boolean] = Reader((c: Config) => c.inDev)
And we can write a plain function that, given some boolean, will read the appropriate value.
def branch(flag: Boolean): Reader[Config, Int] =
if (flag) Reader((c: Config) => c.devValue)
else Reader((c: Config) => c.liveValue)
Now we can compose the two:
val getValue: Reader[Config, Int] =
for {
flag <- inDev
value <- branch(flag)
} yield value
And now we can get our Int
by passing in various Config
values:
getValue.run(Config(true, 1, 2)) // 1
getValue.run(Config(false, 1, 2)) // 2
This in general can be used as a nice way to achieve dependency injection, using just functions!
Upvotes: 4