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))
}
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? ")
_ <- readLn()
name <- write(s"Hello, ${name}!\n")
_ } yield ()
val esProgram: Reader[HasReadLn with HasWrite, Unit] =
for {
<- write("¿Cómo te llamas? ")
_ <- readLn()
name <- write(s"¡Hola, ${name}!\n")
_ } yield ()
val program: Reader[HasEnv with HasReadLn with HasWrite, Unit] =
for {
<- readEnv[HasEnv with HasReadLn with HasWrite]("LANG")
lang <- if (lang.startsWith("es")) {
_
esProgram} else {
enProgram}
} yield ()
.run {
programnew 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)
}
}
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!
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(
.getClass().getClassLoader(),
e0,
interfacesnew InvocationHandler() {
override def invoke(proxy: Object, method: Method, args: Array[AnyRef]): AnyRef = {
if (method.getDeclaringClass().isAssignableFrom(a.getClass())) {
.invoke(a, args: _*)
method} else if (method.getDeclaringClass().isAssignableFrom(b.getClass())) {
.invoke(b, args: _*)
method} 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)
.run(env)
r}
}
}
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)