Reader Monad

Reader

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

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

  def flatMap[E1 <: E, B](f: A => Reader[E1, B]): Reader[E1, B] =
    Reader(e => f(run(e)).run(e))
}

Usage

trait HasEnv {
  def env: Map[String, String]
}

def readEnv[E <: HasEnv](name: String): Reader[E, String] =
  Reader(r => r.env(name))
trait HasReadLn {
  def readLn(): String
}

def readLn[E <: HasReadLn](): Reader[E, String] =
  Reader(r => r.readLn())
trait HasWrite {
  def write(output: String): Unit
}

def write[E <: HasWrite](output: String): Reader[E, Unit] =
  Reader(r => r.write(output))
val enProgram: Reader[HasReadLn with HasWrite, Unit] =
  for {
    _    <- write("What's your name? ")
    name <- readLn()
    _    <- write(s"Hello, ${name}!\n")
  } yield ()

val esProgram: Reader[HasReadLn with HasWrite, Unit] =
  for {
    _    <- write("¿Cómo te llamas? ")
    name <- readLn()
    _    <- write(s"¡Hola, ${name}!\n")
  } yield ()

val program: Reader[HasEnv with HasReadLn with HasWrite, Unit] =
  for {
    lang <- readEnv[HasEnv with HasReadLn with HasWrite]("LANG")
    _    <- if (lang.startsWith("es")) {
              esProgram
            } else {
              enProgram
            }
  } yield ()
program.run {
  new HasEnv with HasReadLn with HasWrite {
    override val env: Map[String, String] = sys.env
    override def readLn(): String = scala.io.StdIn.readLine()
    override def write(output: String): Unit = print(output)
  }
}

Demo

This file is literate Scala, and can be run using Codedown:

$ curl https://earldouglas.com/posts/effect-systems/reader.md |
  codedown scala > script.scala
$ LANG=es scala -nc script.scala
¿Cómo te llamas? James
¡Hola, James!

Experimental

It is possible to use runtime reflection and a proxy to partially inject environments.

implicit class Inject[E, A](r: Reader[E, A]) {

  import scala.reflect.runtime.universe.TypeTag

  def inject[E0: TypeTag, E1: TypeTag](e0: E0)(implicit ev: E0 with E1 <:< E): Reader[E1, A] = {

    import scala.reflect.runtime.universe._

    val t0 = weakTypeTag[E0].tpe
    val t1 = weakTypeTag[E1].tpe

    val interfaces: Array[Class[_]] =
      (t0.baseClasses ++ t1.baseClasses)
        .map(_.asClass.fullName)
        .toSet
        .flatMap { n: String =>
            try {
              Some(Class.forName(n))
            } catch {
              case e: ClassNotFoundException => Option.empty[Class[_]]
              case e: IllegalArgumentException => Option.empty[Class[_]]
            }
        }
        .filter(_.isInterface()) // required by [[Proxy.newProxyInstance]] below
        .toArray

    def proxy[A, B](a: A, b: B): A with B = {
      import java.lang.reflect.Method
      import java.lang.reflect.InvocationHandler
      import java.lang.reflect.Proxy

      Proxy.newProxyInstance(
        e0.getClass().getClassLoader(),
        interfaces,
        new InvocationHandler() {
          override def invoke(proxy: Object, method: Method, args: Array[AnyRef]): AnyRef = {
            if (method.getDeclaringClass().isAssignableFrom(a.getClass())) {
              method.invoke(a, args: _*)
            } else if (method.getDeclaringClass().isAssignableFrom(b.getClass())) {
              method.invoke(b, args: _*)
            } else {
              throw new RuntimeException(s"don't know how to invoke ${method}")
            }
          }
        }
      ).asInstanceOf[A with B]
    }

    Reader { e1: E1 =>
      val env = proxy[E0, E1](e0, e1)
      r.run(env)
    }
  }
}
val hasEnv: HasEnv =
  new HasEnv {
    override val env: Map[String, String] = sys.env
  }

val hasReadLnWithWrite: HasReadLn with HasWrite =
  new HasReadLn with HasWrite {
    override def readLn(): String = scala.io.StdIn.readLine()
    override def write(output: String): Unit = print(output)
  }

program
 .inject[HasEnv, HasReadLn with HasWrite](hasEnv)
 .run(hasReadLnWithWrite)