Som Bhattacharyya
Som Bhattacharyya

Reputation: 4112

What does the map and flat map operations mean for a ReaderMonad

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 monads. 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

Answers (1)

adelbertc
adelbertc

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

Related Questions