Effect systems allow the separation of the semantics of a program from the specification of a program. An effect system is made up of effect types, effectful programs, and effect handlers. An effect is an operation, behavior, pattern, or signature of which the meaning is up for interpretation by an effect handler.
We explore different ways to implement effect systems in Scala. We begin by describing an effectful program that we would like to write as a pure value. We then write different toy effect systems that can run it.
Selections from this page were presented as a series of live-coding sessions:
Imagine a pure functional program that looks something like the following:
val enProgram =
for
<- write("What's your name? ")
_ <- readLn()
name <- write(s"Hello, ${name}!\n")
_ yield ()
val esProgram =
for
<- write("¿Cómo te llamas? ")
_ <- readLn()
name <- write(s"¡Hola, ${name}!\n")
_ yield ()
val program =
for
<- readEnv("LANG")
lang <- if lang.startsWith("es") then esProgram
_ else enProgram
yield ()
This program looks up the $LANG
environment variable,
then performs some simple keyboard and console I/O in the user's
regional language.
This program is itself just a value; it's a representation of a sequence of instructions. On its own, this program doesn't do anything and can't be run. Let's explore how to run it using different effect systems.
Monad transformers allow us to stack monads in way that lets us treat
them as a single monadic type. this lets us "flatten" our
for
comprehensions.
Before:
for {
a <- foo
b <- for {
c <- bar
} yield c
e <- baz
} yield raz
After:
for {
a <- foo
b <- bar
e <- baz
} yield raz
Monad transformers look like they're inside-out.
OptionT[Future, A]
behaves like
Future[Option[A]]
if you wave your hands and squint.
Let's define our monad in the tagless-final style.
trait Monad[M[_]] {
def pure[A](x: A): M[A]
def map[A, B](x: M[A])(f: A => B): M[B] = flatMap(x)(a => pure(f(a)))
def flatMap[A, B](x: M[A])(f: A => M[B]): M[B]
}
implicit class MonadOps[M[_]: Monad, A](ma: M[A]) {
val M = implicitly[Monad[M]]
def map[B](f: A => B): M[B] = M.map(ma)(f)
def flatMap[B](f: A => M[B]): M[B] = M.flatMap(ma)(f)
}
We'll need a monadic type at the bottom of our monad transformer stack that gives us access to its own value.
type ID[A] = A
implicit val idMonad: Monad[ID] =
new Monad[ID] {
override def pure[A](x: A): ID[A] = x
override def flatMap[A, B](x: ID[A])(f: A => ID[B]): ID[B] = f(x)
}
Our environment-reading effect abstracts over a continuation from the environment to a value.
abstract class ReadEnvT[F[_]: Monad, A] {
def runEnv(env: Map[String, String]): F[A]
}
implicit def readEnvTMonad[F[_]: Monad]: Monad[({type λ[α] = ReadEnvT[F, α]})#λ] =
new Monad[({type λ[α] = ReadEnvT[F, α]})#λ] {
val F = implicitly[Monad[F]]
override def pure[A](x: A): ReadEnvT[F, A] =
new ReadEnvT[F, A] {
override def runEnv(env: Map[String, String]): F[A] =
.pure(x)
F}
override def flatMap[A, B](x: ReadEnvT[F, A])(f: A => ReadEnvT[F, B]): ReadEnvT[F, B] =
new ReadEnvT[F, B] {
override def runEnv(env: Map[String, String]): F[B] =
.flatMap(F.map(x.runEnv(env))(f))(_.runEnv(env))
F}
}
Our line-reading effect abstracts over a continuation from a line-reading function to a value.
abstract class ReadLnT[F[_]: Monad, A] {
def runIn(readLn: () => String): F[A]
}
implicit def readLnTMonad[F[_]: Monad]: Monad[({type λ[α] = ReadLnT[F, α]})#λ] =
new Monad[({type λ[α] = ReadLnT[F, α]})#λ] {
val F = implicitly[Monad[F]]
override def pure[A](x: A): ReadLnT[F, A] =
new ReadLnT[F, A] {
override def runIn(readLn: () => String): F[A] =
.pure(x)
F}
override def flatMap[A, B](x: ReadLnT[F, A])(f: A => ReadLnT[F, B]): ReadLnT[F, B] =
new ReadLnT[F, B] {
override def runIn(readLn: () => String): F[B] =
.flatMap(F.map(x.runIn(readLn))(f))(_.runIn(readLn))
F}
}
Our line-reading effect abstracts over a continuation from a line-writing function to a value.
abstract class WriteT[F[_]: Monad, A] {
def runOut(write: String => Unit): F[A]
}
implicit def writeTMonad[F[_]: Monad]: Monad[({type λ[α] = WriteT[F, α]})#λ] =
new Monad[({type λ[α] = WriteT[F, α]})#λ] {
val F = implicitly[Monad[F]]
override def pure[A](x: A): WriteT[F, A] =
new WriteT[F, A] {
override def runOut(write: String => Unit): F[A] =
.pure(x)
F}
override def flatMap[A, B](x: WriteT[F, A])(f: A => WriteT[F, B]): WriteT[F, B] =
new WriteT[F, B] {
override def runOut(write: String => Unit): F[B] =
.flatMap(F.map(x.runOut(write))(f))(_.runOut(write))
F}
}
This is more tolerable with Kind Projector. My apologies to your eyes.
type Program[A] = WriteT[({type λ[α] = ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, α] })#λ, A]
def readEnv(name: String): Program[String] =
new WriteT[({type λ[α] = ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, α] })#λ, String] {
def runOut(write: String => Unit): ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, String] = {
new ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, String] {
override def runEnv(env: Map[String, String]): ReadLnT[ID, String] =
new ReadLnT[ID, String] {
override def runIn(readLn: () => String): String = {
env(name)
}
}
}
}
}
def readLn(): Program[String] =
new WriteT[({type λ[α] = ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, α] })#λ, String] {
def runOut(write: String => Unit): ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, String] = {
new ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, String] {
override def runEnv(env: Map[String, String]): ReadLnT[ID, String] =
new ReadLnT[ID, String] {
override def runIn(readLn: () => String): String = {
readLn()
}
}
}
}
}
def write(output: String): Program[Unit] =
new WriteT[({type λ[α] = ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, α] })#λ, Unit] {
def runOut(write: String => Unit): ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, Unit] = {
new ReadEnvT[({type λ[α] = ReadLnT[({type λ[α] = ID[α]})#λ, α]})#λ, Unit] {
override def runEnv(env: Map[String, String]): ReadLnT[ID, Unit] =
new ReadLnT[ID, Unit] {
override def runIn(readLn: () => String): Unit = {
write(output)
}
}
}
}
}
Finally, here's our demo program.
val enProgram: Program[Unit] =
for {
<- write("What's your name? ")
_ <- readLn()
name <- write(s"Hello, ${name}!\n")
_ } yield ()
val esProgram: Program[Unit] =
for {
<- write("¿Cómo te llamas? ")
_ <- readLn()
name <- write(s"¡Hola, ${name}!\n")
_ } yield ()
val program: Program[Unit] =
for {
<- readEnv("LANG")
lang <- if (lang.startsWith("es")) {
_
esProgram} else {
enProgram}
} yield ()
We can run it by iteratively supplying each of the side-effecting bits.
program.runOut(print)
.runEnv(sys.env)
.runIn(scala.io.StdIn.readLine)
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli --scala 2.13 <(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Monad transformers in Scala 2'
)
¿Cómo te llamas? James
¡Hola, James!
Let's define our monad in the tagless-final style.
trait Monad[M[_]] {
def pure[A](x: A): M[A]
def map[A, B](x: M[A])(f: A => B): M[B] = flatMap(x)(a => pure(f(a)))
def flatMap[A, B](x: M[A])(f: A => M[B]): M[B]
}
implicit class MonadOps[M[_]: Monad, A](ma: M[A]) {
val M = implicitly[Monad[M]]
def map[B](f: A => B): M[B] = M.map(ma)(f)
def flatMap[B](f: A => M[B]): M[B] = M.flatMap(ma)(f)
}
We'll need a monadic type at the bottom of our monad transformer stack that gives us access to its own value.
type ID[A] = A
implicit val idMonad: Monad[ID] =
new Monad[ID] {
override def pure[A](x: A): ID[A] = x
override def flatMap[A, B](x: ID[A])(f: A => ID[B]): ID[B] = f(x)
}
Our environment-reading effect abstracts over a continuation from the environment to a value.
abstract class ReadEnvT[F[_]: Monad, A] {
def runEnv(env: Map[String, String]): F[A]
}
implicit def readEnvTMonad[F[_]: Monad]: Monad[ReadEnvT[F, *]] =
new Monad[ReadEnvT[F, *]] {
val F = implicitly[Monad[F]]
override def pure[A](x: A): ReadEnvT[F, A] =
new ReadEnvT[F, A] {
override def runEnv(env: Map[String, String]): F[A] =
.pure(x)
F}
override def flatMap[A, B](x: ReadEnvT[F, A])(f: A => ReadEnvT[F, B]): ReadEnvT[F, B] =
new ReadEnvT[F, B] {
override def runEnv(env: Map[String, String]): F[B] =
.flatMap(F.map(x.runEnv(env))(f))(_.runEnv(env))
F}
}
Our line-reading effect abstracts over a continuation from a line-reading function to a value.
abstract class ReadLnT[F[_]: Monad, A] {
def runIn(readLn: () => String): F[A]
}
implicit def readLnTMonad[F[_]: Monad]: Monad[ReadLnT[F, *]] =
new Monad[ReadLnT[F, *]] {
val F = implicitly[Monad[F]]
override def pure[A](x: A): ReadLnT[F, A] =
new ReadLnT[F, A] {
override def runIn(readLn: () => String): F[A] =
.pure(x)
F}
override def flatMap[A, B](x: ReadLnT[F, A])(f: A => ReadLnT[F, B]): ReadLnT[F, B] =
new ReadLnT[F, B] {
override def runIn(readLn: () => String): F[B] =
.flatMap(F.map(x.runIn(readLn))(f))(_.runIn(readLn))
F}
}
Our line-reading effect abstracts over a continuation from a line-writing function to a value.
abstract class WriteT[F[_]: Monad, A] {
def runOut(write: String => Unit): F[A]
}
implicit def writeTMonad[F[_]: Monad]: Monad[WriteT[F, *]] =
new Monad[WriteT[F, *]] {
val F = implicitly[Monad[F]]
override def pure[A](x: A): WriteT[F, A] =
new WriteT[F, A] {
override def runOut(write: String => Unit): F[A] =
.pure(x)
F}
override def flatMap[A, B](x: WriteT[F, A])(f: A => WriteT[F, B]): WriteT[F, B] =
new WriteT[F, B] {
override def runOut(write: String => Unit): F[B] =
.flatMap(F.map(x.runOut(write))(f))(_.runOut(write))
F}
}
This is more tolerable with kind-projector. Apologies to your eyes.
type Program[A] = WriteT[ReadEnvT[ReadLnT[ID[*], *], *], A]
def readEnv(name: String): Program[String] =
new WriteT[ReadEnvT[ReadLnT[ID[*], *], *], String] {
def runOut(write: String => Unit): ReadEnvT[ReadLnT[ID[*], *], String] = {
new ReadEnvT[ReadLnT[ID[*], *], String] {
override def runEnv(env: Map[String, String]): ReadLnT[ID, String] =
new ReadLnT[ID, String] {
override def runIn(readLn: () => String): String = {
env(name)
}
}
}
}
}
def readLn(): Program[String] =
new WriteT[ReadEnvT[ReadLnT[ID[*], *], *], String] {
def runOut(write: String => Unit): ReadEnvT[ReadLnT[ID[*], *], String] = {
new ReadEnvT[ReadLnT[ID[*], *], String] {
override def runEnv(env: Map[String, String]): ReadLnT[ID, String] =
new ReadLnT[ID, String] {
override def runIn(readLn: () => String): String = {
readLn()
}
}
}
}
}
def write(output: String): Program[Unit] =
new WriteT[ReadEnvT[ReadLnT[ID[*], *], *], Unit] {
def runOut(write: String => Unit): ReadEnvT[ReadLnT[ID[*], *], Unit] = {
new ReadEnvT[ReadLnT[ID[*], *], Unit] {
override def runEnv(env: Map[String, String]): ReadLnT[ID, Unit] =
new ReadLnT[ID, Unit] {
override def runIn(readLn: () => String): Unit = {
write(output)
}
}
}
}
}
Finally, here's our demo program.
val enProgram: Program[Unit] =
for {
<- write("What's your name? ")
_ <- readLn()
name <- write(s"Hello, ${name}!\n")
_ } yield ()
val esProgram: Program[Unit] =
for {
<- write("¿Cómo te llamas? ")
_ <- readLn()
name <- write(s"¡Hola, ${name}!\n")
_ } yield ()
val program: Program[Unit] =
for {
<- readEnv("LANG")
lang <- if (lang.startsWith("es")) {
_
esProgram} else {
enProgram}
} yield ()
We can run it by iteratively supplying each of the side-effecting bits.
program.runOut(print)
.runEnv(sys.env)
.runIn(scala.io.StdIn.readLine)
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 2.13 \
--compiler-plugin org.typelevel:::kind-projector:0.13.2 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Monad transformers in Scala 2 with Kind Projector'
)
¿Cómo te llamas? James
¡Hola, James!
Let's define our monad in the tagless-final style.
trait Monad[M[_]]:
def pure[A](x: A): M[A]
def map[A, B](x: M[A])(f: A => B): M[B] = flatMap(x)(a => pure(f(a)))
def flatMap[A, B](x: M[A])(f: A => M[B]): M[B]
implicit class MonadOps[M[_]: Monad, A](ma: M[A]):
val M = implicitly[Monad[M]]
def map[B](f: A => B): M[B] = M.map(ma)(f)
def flatMap[B](f: A => M[B]): M[B] = M.flatMap(ma)(f)
We'll need a monadic type at the bottom of our monad transformer stack that gives us access to its own value.
type ID[A] = A
implicit val idMonad: Monad[ID] =
new Monad[ID]:
override def pure[A](x: A): ID[A] = x
override def flatMap[A, B](x: ID[A])(f: A => ID[B]): ID[B] = f(x)
Our environment-reading effect abstracts over a continuation from the environment to a value.
abstract class ReadEnvT[F[_]: Monad, A]:
def runEnv(env: Map[String, String]): F[A]
implicit def readEnvTMonad[F[_]: Monad]: Monad[ReadEnvT[F, _]] =
new Monad[ReadEnvT[F, _]]:
val F = implicitly[Monad[F]]
override def pure[A](x: A): ReadEnvT[F, A] =
new ReadEnvT[F, A]:
override def runEnv(env: Map[String, String]): F[A] =
.pure(x)
F
override def flatMap[A, B](x: ReadEnvT[F, A])(f: A => ReadEnvT[F, B]): ReadEnvT[F, B] =
new ReadEnvT[F, B]:
override def runEnv(env: Map[String, String]): F[B] =
.flatMap(F.map(x.runEnv(env))(f))(_.runEnv(env)) F
Our line-reading effect abstracts over a continuation from a line-reading function to a value.
abstract class ReadLnT[F[_]: Monad, A]:
def runIn(readLn: () => String): F[A]
implicit def readLnTMonad[F[_]: Monad]: Monad[ReadLnT[F, _]] =
new Monad[ReadLnT[F, _]]:
val F = implicitly[Monad[F]]
override def pure[A](x: A): ReadLnT[F, A] =
new ReadLnT[F, A]:
override def runIn(readLn: () => String): F[A] =
.pure(x)
F
override def flatMap[A, B](x: ReadLnT[F, A])(f: A => ReadLnT[F, B]): ReadLnT[F, B] =
new ReadLnT[F, B]:
override def runIn(readLn: () => String): F[B] =
.flatMap(F.map(x.runIn(readLn))(f))(_.runIn(readLn)) F
Our line-reading effect abstracts over a continuation from a line-writing function to a value.
abstract class WriteT[F[_]: Monad, A]:
def runOut(write: String => Unit): F[A]
implicit def writeTMonad[F[_]: Monad]: Monad[WriteT[F, _]] =
new Monad[WriteT[F, _]]:
val F = implicitly[Monad[F]]
override def pure[A](x: A): WriteT[F, A] =
new WriteT[F, A]:
override def runOut(write: String => Unit): F[A] =
.pure(x)
F
override def flatMap[A, B](x: WriteT[F, A])(f: A => WriteT[F, B]): WriteT[F, B] =
new WriteT[F, B]:
override def runOut(write: String => Unit): F[B] =
.flatMap(F.map(x.runOut(write))(f))(_.runOut(write)) F
This is more tolerable with kind-projector. Apologies to your eyes.
type Program[A] = WriteT[ReadEnvT[ReadLnT[ID[_], _], _], A]
def readEnv(name: String): Program[String] =
new WriteT[ReadEnvT[ReadLnT[ID[_], _], _], String]:
def runOut(write: String => Unit): ReadEnvT[ReadLnT[ID[_], _], String] =
new ReadEnvT[ReadLnT[ID[_], _], String]:
override def runEnv(env: Map[String, String]): ReadLnT[ID, String] =
new ReadLnT[ID, String]:
override def runIn(readLn: () => String): String =
env(name)
def readLn(): Program[String] =
new WriteT[ReadEnvT[ReadLnT[ID[_], _], _], String]:
def runOut(write: String => Unit): ReadEnvT[ReadLnT[ID[_], _], String] =
new ReadEnvT[ReadLnT[ID[_], _], String]:
override def runEnv(env: Map[String, String]): ReadLnT[ID, String] =
new ReadLnT[ID, String]:
override def runIn(readLn: () => String): String =
readLn()
def write(output: String): Program[Unit] =
new WriteT[ReadEnvT[ReadLnT[ID[_], _], _], Unit]:
def runOut(write: String => Unit): ReadEnvT[ReadLnT[ID[_], _], Unit] =
new ReadEnvT[ReadLnT[ID[_], _], Unit]:
override def runEnv(env: Map[String, String]): ReadLnT[ID, Unit] =
new ReadLnT[ID, Unit]:
override def runIn(readLn: () => String): Unit =
write(output)
Finally, here's our demo program.
val enProgram: Program[Unit] =
for
<- write("What's your name? ")
_ <- readLn()
name <- write(s"Hello, ${name}!\n")
_ yield ()
val esProgram: Program[Unit] =
for
<- write("¿Cómo te llamas? ")
_ <- readLn()
name <- write(s"¡Hola, ${name}!\n")
_ yield ()
val program: Program[Unit] =
for
<- readEnv("LANG")
lang <- if (lang.startsWith("es")) {
_
esProgram} else {
enProgram}
yield ()
We can run it by iteratively supplying each of the side-effecting bits.
program.runOut(print)
.runEnv(sys.env)
.runIn(scala.io.StdIn.readLine)
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 3.3 \
-Ykind-projector:underscores \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Monad transformers in Scala 3'
)
¿Cómo te llamas? James
¡Hola, James!
Real-world programs can be abstracted as operations that read an external context and either produce a value or fail with an error. Here we look at how monad transformers can be stacked into a practical general-purpose effect system.
Given:
EitherT[F[_], A, B]
encapsulates
F[Either[A, B]]
FutureT[F[_], A]
encapsulates
F[Future[A]]
Reader[A, B]
is simply A => B
Let:
Effect[R, E, A]
is
EitherT[FutureT[Reader[R, A]], E, A]
import scala.concurrent.{ExecutionContext => EC}
import scala.concurrent.Future
case class Effect[-R, +E, +A](unsafeRun: R => Future[Either[E, A]])(implicit ec: EC):
def map[B](f: A => B)(implicit ec: EC): Effect[R, E, B] =
Effect(r =>
unsafeRun(r).map {
case Left(e) => Left(e)
case Right(a) => Right(f(a))
}
)
def flatMap[R2 <: R, E2 >: E, B](f: A => Effect[R2, E2, B])(implicit ec: EC): Effect[R2, E2, B] =
Effect(r =>
unsafeRun(r).flatMap {
case Left(e) => Future(Left(e))
case Right(a) => f(a).unsafeRun(r)
}
)
def >>[R2 <: R, E2 >: E, B](x: Effect[R2, E2, B])(implicit ec: EC): Effect[R2, E2, B] =
flatMap(_ => x)
object Effect:
def success[E, A](a: => A)(implicit ec: EC): Effect[Any, E, A] =
Effect(_ => Future(Right(a)))
def failure[E, A](e: => E)(implicit ec: EC): Effect[Any, E, A] =
Effect(_ => Future(Left(e)))
sealed trait JdbcError
case class Caught(throwable: Throwable) extends JdbcError
case class NotFound(message: String) extends JdbcError
object Jdbc:
import java.sql.Connection
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.util.Failure
import scala.util.Success
import scala.util.Try
type Jdbc[A] = Effect[Connection, JdbcError, A]
def transact[A](s: Jdbc[A], c: Connection)(implicit ec: EC): Future[Either[JdbcError, A]] =
.synchronized {
cval ac = c.getAutoCommit
.setAutoCommit(false)
cval ea =
.unsafeRun(c).map {
scase r @ Right(_) =>
.commit()
c
rcase l @ Left(_) =>
.rollback()
c
l}
.setAutoCommit(ac)
c
ea}
def unsafeRun[A](s: Jdbc[A], c: Connection)(implicit ec: EC): Either[JdbcError, A] =
.result(Jdbc.transact(s, c), Duration(100, "millis"))
Await
def update(u: String)(implicit ec: EC): Jdbc[Unit] =
Effect(c =>
Future(
{
Try val s = c.createStatement()
.executeUpdate(u)
s.close()
s} match {
case Success(_) => Right(())
case Failure(t) => Left(Caught(t))
}
)
)
def query[A](k: Connection => A)(implicit ec: EC): Jdbc[A] =
Effect(c =>
Future(
{
Try k(c)
} match {
case Success(x) => Right(x)
case Failure(t) => Left(Caught(t))
}
)
)
object MyDatabase:
import Jdbc.Jdbc
def init(implicit ec: EC): Jdbc[Unit] =
.update(
Jdbc"""|CREATE TABLE MESSAGES (
| `LANG` VARCHAR(5),
| `VALUE` VARCHAR(128)
|)""".stripMargin
) >>
.update(
Jdbc"""|INSERT INTO MESSAGES (`LANG`, `VALUE`)
| VALUES ('en', 'Hello, world!')
|""".stripMargin
) >>
.update(
Jdbc"""|INSERT INTO MESSAGES (`LANG`, `VALUE`)
|VALUES ('es', '¡Hola, mundo!')
|""".stripMargin
)
def get(lang: String)(implicit ec: EC): Jdbc[String] =
Jdbc.query(c =>
val q = "SELECT `VALUE` FROM MESSAGES WHERE `LANG` = ?"
val s = c.prepareStatement(q)
.setString(1, lang)
sval r = s.executeQuery()
val vo = if (r.next()) then Some(r.getString("VALUE")) else None
.close()
s
vo)
.flatMap {
case Some(x) =>
.success(x)
Effectcase None =>
.failure(NotFound(s"no message for lang=${lang}"))
Effect}
object MyProgram:
import Jdbc.Jdbc
def succeeds(implicit ec: EC): Jdbc[Map[String, String]] =
for
<- MyDatabase.get("en")
en <- MyDatabase.get("es")
es yield Map("en" -> en, "es" -> es)
def fails(implicit ec: EC): Jdbc[Map[String, String]] =
for
<- MyDatabase.get("fr")
fr <- MyDatabase.get("es")
es yield Map("fr" -> fr, "es" -> es)
implicit val ec: EC = EC.global
Class.forName("org.h2.Driver")
val c = java.sql.DriverManager.getConnection("jdbc:h2:mem:db", "sa", "")
println(Jdbc.unsafeRun(MyDatabase.init, c))
// Right(())
println(Jdbc.unsafeRun(MyProgram.succeeds, c))
// Right(Map(en -> Hello, world!, es -> ¡Hola, mundo!))
println(Jdbc.unsafeRun(MyProgram.fails, c))
// Left(NotFound(no message for lang=fr))
This code is literate Scala, and can be run using Codedown:
$ scala-cli \
--scala 3.3 \
--dep com.h2database:h2:2.2.224 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Stacked monad transformers in practice'
)
Right(())
Right(Map(en -> Hello, world!, es -> ¡Hola, mundo!))
Left(NotFound(no message for lang=fr))
Free monads are the defunctionalization of tagless-final.
trait ~>[-F[_], +G[_]]:
def apply[A](f: F[A]): G[A]
trait Monad[M[_]]:
def pure[A](x: A): M[A]
def map[A, B](x: M[A])(f: A => B): M[B] = flatMap(x)(a => pure(f(a)))
def flatMap[A, B](x: M[A])(f: A => M[B]): M[B]
type ID[A] = A
: Monad[ID] =
given idMonadnew Monad[ID]:
override def pure[A](x: A): ID[A] = x
override def flatMap[A, B](x: ID[A])(f: A => ID[B]): ID[B] = f(x)
[F[_], A]:
enum Free
import Free._
def map[B](f: A => B): Free[F, B] =
{ a => pure(f(a)) }
flatMap
def flatMap[B](f: A => Free[_ <: F, B]): Free[F, B] =
Bind(this, f)
def foldMap[G[_]: Monad](nt: F ~> G): G[A] =
this match
case Pure(a) =>
[Monad[G]].pure(a)
summoncase Suspend(fa) =>
nt(fa)
case Bind(fa, f) =>
val mg = summon[Monad[G]]
val ga = fa.foldMap(nt)
.flatMap(ga)(f(_).foldMap(nt))
mg
case Pure[F[_], A](a: A) extends Free[F, A]
case Suspend[F[_], A](fa: F[A]) extends Free[F, A]
case Bind[F[_], A, B](fa: Free[F, A], f: A => Free[_ <: F, B]) extends Free[F, B]
object Free:
def pure[F[_], A](a: A): Free[F, A] = Pure(a)
def liftM[F[_], A](fa: F[A]): Free[F, A] = Suspend(fa)
[A]:
enum EnvAlgebracase ReadEnv(name: String) extends EnvAlgebra[String]
[A]:
enum StdioAlgebracase ReadLn extends StdioAlgebra[String]
case Write(output: String) extends StdioAlgebra[Unit]
val enProgram: Free[StdioAlgebra, Unit] =
for
<- Free.liftM(StdioAlgebra.Write("What's your name? "))
_ <- Free.liftM(StdioAlgebra.ReadLn)
name <- Free.liftM(StdioAlgebra.Write(s"Hello, ${name}!\n"))
_ yield ()
val esProgram: Free[StdioAlgebra, Unit] =
for
<- Free.liftM(StdioAlgebra.Write("¿Cómo te llamas? "))
_ <- Free.liftM(StdioAlgebra.ReadLn)
name <- Free.liftM(StdioAlgebra.Write(s"¡Hola, ${name}!\n"))
_ yield ()
val program: Free[[A] =>> StdioAlgebra[A] | EnvAlgebra[A], Unit] =
for
<- Free.liftM(EnvAlgebra.ReadEnv("LANG"))
lang <- if (lang.startsWith("es")) then esProgram
_ else enProgram
yield ()
.foldMap[ID] {
programnew ~>[[A] =>> StdioAlgebra[A] | EnvAlgebra[A], ID]:
def apply[A](x: StdioAlgebra[A] | EnvAlgebra[A]): ID[A] =
match
x case EnvAlgebra.ReadEnv(name) => sys.env(name)
case StdioAlgebra.ReadLn => scala.io.StdIn.readLine()
case StdioAlgebra.Write(output) => print(output)
}
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 3.3 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Free monads in Scala 3'
)
¿Cómo te llamas? James
¡Hola, James!
Tagless-final is the Church encoding of free monads.
trait Monad[M[_]]:
def pure[A](x: A): M[A]
def map[A, B](x: M[A])(f: A => B): M[B] = flatMap(x)(a => pure(f(a)))
def flatMap[A, B](x: M[A])(f: A => M[B]): M[B]
[M[_]: Monad, A](ma: M[A])
extension def map[B](f: A => B): M[B] = summon[Monad[M]].map(ma)(f)
def flatMap[B](f: A => M[B]): M[B] = summon[Monad[M]].flatMap(ma)(f)
type ID[A] = A
: Monad[ID] =
given idMonadnew Monad[ID]:
override def pure[A](x: A): ID[A] = x
override def flatMap[A, B](x: ID[A])(f: A => ID[B]): ID[B] = f(x)
trait EnvAlgebra[F[_]]:
def readEnv(name: String): F[String]
trait StdioAlgebra[F[_]]:
def readLn: F[String]
def write(output: String): F[Unit]
def enProgram[F[_]: Monad: StdioAlgebra](): F[Unit] =
for
<- summon[StdioAlgebra[F]].write("What's your name? ")
_ <- summon[StdioAlgebra[F]].readLn
name <- summon[StdioAlgebra[F]].write(s"Hello, ${name}!\n")
_ yield ()
def esProgram[F[_]: Monad: StdioAlgebra](): F[Unit] =
for
<- summon[StdioAlgebra[F]].write("¿Cómo te llamas? ")
_ <- summon[StdioAlgebra[F]].readLn
name <- summon[StdioAlgebra[F]].write(s"¡Hola, ${name}!\n")
_ yield ()
def program[F[_]: Monad: StdioAlgebra: EnvAlgebra]: F[Unit] =
for
<- summon[EnvAlgebra[F]].readEnv("LANG")
lang <- if (lang.startsWith("es")) then esProgram()
_ else enProgram()
yield ()
: EnvAlgebra[ID] =
given liveEnvnew EnvAlgebra[ID]:
override def readEnv(name: String): ID[String] = sys.env(name)
: StdioAlgebra[ID] =
given liveStdionew StdioAlgebra[ID]:
override def readLn: ID[String] = scala.io.StdIn.readLine()
override def write(output: String): ID[Unit] = print(output)
program
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 3.3 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Tagless-final in Scala 3'
)
¿Cómo te llamas? James
¡Hola, James!
trait EnvAlgebra[F[_]]:
def readEnv(name: String): F[String]
trait StdioAlgebra[F[_]]:
def readLn: F[String]
def write(output: String): F[Unit]
import cats.Id
import cats.Monad
import cats.implicits.toFunctorOps // `map` extension method
import cats.implicits.toFlatMapOps // `flatMap` extension method
def enProgram[F[_]: Monad: StdioAlgebra](): F[Unit] =
for
<- summon[StdioAlgebra[F]].write("What's your name? ")
_ <- summon[StdioAlgebra[F]].readLn
name <- summon[StdioAlgebra[F]].write(s"Hello, ${name}!\n")
_ yield ()
def esProgram[F[_]: Monad: StdioAlgebra](): F[Unit] =
for
<- summon[StdioAlgebra[F]].write("¿Cómo te llamas? ")
_ <- summon[StdioAlgebra[F]].readLn
name <- summon[StdioAlgebra[F]].write(s"¡Hola, ${name}!\n")
_ yield ()
def program[F[_]: Monad: StdioAlgebra: EnvAlgebra]: F[Unit] =
for
<- summon[EnvAlgebra[F]].readEnv("LANG")
lang <- if (lang.startsWith("es")) then esProgram()
_ else enProgram()
yield ()
: EnvAlgebra[Id] =
given liveEnvnew EnvAlgebra[Id]:
override def readEnv(name: String): Id[String] = sys.env(name)
: StdioAlgebra[Id] =
given liveStdionew StdioAlgebra[Id]:
override def readLn: Id[String] = scala.io.StdIn.readLine()
override def write(output: String): Id[Unit] = print(output)
[Id] program
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 3.3 \
--dependency org.typelevel::cats-core:2.10.0 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Tagless-final via Cats'
)
¿Cómo te llamas? James
¡Hola, James!
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)
}
}
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)
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 2.13 \
--dependency org.scala-lang:scala-reflect:2.13.12 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Reader monad in Scala 2'
)
¿Cómo te llamas? James
¡Hola, James!
trait HasEnv {
def env: Map[String, String]
}
def readEnv(name: String): zio.ZIO[HasEnv, Throwable, String] =
.ZIO.environmentWithZIO { r =>
zio.ZIO.attempt {
zio.get.env(name)
r}
}
trait HasReadLn {
def readLn(): String
}
def readLn: zio.ZIO[HasReadLn, Throwable, String] =
.ZIO.environmentWithZIO { r =>
zio.ZIO.attempt {
zio.get.readLn()
r}
}
trait HasWrite {
def write(output: String): Unit
}
def write(output: String): zio.ZIO[HasWrite, Throwable, Unit] =
.ZIO.environmentWithZIO { r =>
zio.ZIO.attempt {
zio.get.write(output)
r}
}
def enProgram: zio.ZIO[HasReadLn with HasWrite, Throwable, Unit] =
for {
<- write("What's your name? ")
_ <- readLn
name <- write(s"Hello, ${name}!\n")
_ } yield ()
def esProgram: zio.ZIO[HasReadLn with HasWrite, Throwable, Unit] =
for {
<- write("¿Cómo te llamas? ")
_ <- readLn
name <- write(s"¡Hola, ${name}!\n")
_ } yield ()
def program: zio.ZIO[HasEnv with HasReadLn with HasWrite, Throwable, Unit] =
for {
<- readEnv("LANG")
lang <- if (lang.startsWith("es")) {
_
esProgram} else {
enProgram}
} yield ()
.Unsafe.unsafely {
zio//zio.Unsafe.unsafe { implicit u: zio.Unsafe =>
.Runtime.default.unsafe.run(
zio.provide(
program.ZLayer.succeed(
zionew HasEnv {
override def env: Map[String, String] = sys.env
}
),
.ZLayer.succeed(
zionew HasReadLn {
override def readLn(): String = scala.io.StdIn.readLine()
}
),
.ZLayer.succeed(
zionew HasWrite {
override def write(output: String): Unit = print(output)
}
)
)
).getOrThrowFiberFailure()
}
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 3.3 \
--dependency dev.zio::zio:2.0.21 \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Reader monad via ZIO'
)
¿Cómo te llamas? James
¡Hola, James!
import java.io.*;
import java.util.*;
import java.util.function.*;
class Reader<R, A> {
private final Function<R, A> k;
run(final R r) {
A return this.k.apply(r);
}
Reader(final Function<R, A> k) {
this.k = k;
}
static <A, B> Reader<A, B> fromFunction(final Function<A, B> k) {
return new Reader<>((a) -> k.apply(a));
}
static <A> Reader<A, Void> fromConsumer(final Consumer<A> k) {
return new Reader<>((a) -> {
.accept(a);
kreturn null;
});
}
<R2 extends R, B> Reader<R2, B> map(final Function<A, B> f) {
return new Reader<>((r) -> f.apply(run(r)));
}
<R2 extends R, B> Reader<R2, B> flatMap(final Function<A, Reader<R2, B>> f) {
return new Reader<>((r) -> f.apply(run(r)).run(r));
}
<R2 extends R, B> Reader<R2, B> andThen(final Reader<R2, B> next) {
return new Reader<>((r) -> {
run(r);
return next.run(r);
});
}
}
interface GetEnv {
Map<String, String> getEnv();
static <R extends GetEnv> Reader<R, Map<String, String>> of() {
return Reader.fromFunction((r) -> r.getEnv());
}
}
interface ReadLine {
String readLine();
static <R extends ReadLine> Reader<R, String> of() {
return Reader.fromFunction((r) -> r.readLine());
}
}
interface WriteLine {
void writeLine(final String line);
static <R extends WriteLine> Reader<R, Void> of(final String line) {
return Reader.fromConsumer((r) -> r.writeLine(line));
}
}
class Greeter {
static <R extends ReadLine & WriteLine> Reader<R, Void> esProgram() {
return WriteLine.of("¿Cómo te llamas?")
.andThen(ReadLine.of())
.flatMap((name) -> WriteLine.of(String.format("¡Hola, %s!", name)));
}
static <R extends GetEnv & ReadLine & WriteLine> Reader<R, Void> enProgram() {
return WriteLine.of("What is your name?")
.andThen(ReadLine.of())
.flatMap((name) -> WriteLine.of(String.format("Hello, %s!", name)));
}
static <R extends GetEnv & ReadLine & WriteLine> Reader<R, Void> program() {
return GetEnv.of()
.flatMap((env) -> {
final String lang =
.ofNullable(env.get("LANG"))
Optional.map((x) -> x.substring(0, 2))
.orElse("en");
switch (lang) {
case "es": return esProgram();
default: return enProgram();
}
});
}
}
class Live {
interface Env extends GetEnv, ReadLine, WriteLine { }
static Env env =
new Env() {
@Override
public Map<String, String> getEnv() {
return System.getenv();
}
@Override
public String readLine() {
final java.io.BufferedReader r =
new BufferedReader(
new InputStreamReader(System.in));
try {
return r.readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void writeLine(final String line) {
System.out.println(line);
}
};
}
class Test {
static final LinkedList<String> output =
new LinkedList<>();
static final LinkedList<String> input =
new LinkedList<>(Arrays.asList("James"));
interface Env extends GetEnv, ReadLine, WriteLine { }
static Env env =
new Env() {
@Override
public Map<String, String> getEnv() {
return Map.ofEntries(Map.entry("LANG", "en_US.UTF-8"));
}
@Override
public String readLine() {
return input.pop();
}
@Override
public void writeLine(final String line) {
.push(line);
output}
};
}
public class Main {
public static void main(String[] args) {
if ("live".equalsIgnoreCase(System.getenv("ENV"))) {
.program().run(Live.env);
Greeter} else {
.program().run(Test.env);
GreeterSystem.out.println(String.format("produced: %s", Test.output));
System.out.println(String.format("unconsumed input: %s", Test.input));
}
}
}
This code is literate Java, and can be run using Codedown:
$ curl https://earldouglas.com/effect-systems.md |
codedown java --section '### Reader monad in Java' > Main.java
$ javac Main.java
$ ENV=live LANG=es_MX.UTF-8 java Main
¿Cómo te llamas?
James
¡Hola, James!
// Workaround for https://github.com/VirtusLab/scala-cli/issues/2653
//> using option -P:continuations:enable
import scala.util.continuations.cpsParam
import scala.util.continuations.reset
import scala.util.continuations.shift
import scala.collection.mutable.Stack
private val ks: Stack[HasEnv with HasReadLn with HasWrite => Unit] =
Stack.empty[HasEnv with HasReadLn with HasWrite => Unit]
trait HasEnv {
def env: Map[String, String]
}
def readEnv(name: String): String@cpsParam[Unit, Unit] =
{ (k: String => Unit) =>
shift .push({ r: HasEnv => k(r.env(name)) })
ks()
}
trait HasReadLn {
def readLn(): String
}
def readLn(): String@cpsParam[Unit, Unit] =
{ (k: String => Unit) =>
shift .push({ r: HasReadLn => k(r.readLn()) })
ks()
}
trait HasWrite {
def write(output: String): Unit
}
def write(output: String): Unit@cpsParam[Unit, Unit] =
{ (k: Unit => Unit) =>
shift .push({ r: HasWrite =>
ks.write(output)
rk()
})
()
}
def enProgram(): Unit@cpsParam[Unit, Unit] = {
write("What's your name? ")
val name: String = readLn()
write(s"Hello, ${name}!\n")
}
def esProgram(): Unit@cpsParam[Unit, Unit] = {
write("¿Cómo te llamas? ")
val name: String = readLn()
write(s"¡Hola, ${name}!\n")
}
def program(): Unit@cpsParam[Unit, Unit] = {
val lang: String = readEnv("LANG")
if (lang.startsWith("es")) {
esProgram()
} else {
enProgram()
}
}
val r: HasEnv with HasReadLn with HasWrite =
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)
}
{
reset program()
}
while (ks.nonEmpty) {
val k: HasEnv with HasReadLn with HasWrite => Unit = ks.pop()
k(r)
}
See the compiler plugin support section of the sbt documentation for the latest configuration information.
For sbt 1.0 and Scala 2.12, use the following:
/***
:= "2.12.2"
scalaVersion
( "org.scala-lang.plugins"
addCompilerPlugin% "scala-continuations-plugin_2.12.2"
% "1.0.3"
)
+=
libraryDependencies "org.scala-lang.plugins" %% "scala-continuations-library" % "1.0.3"
+= "-P:continuations:enable"
scalacOptions */
This code is literate Scala, and can be run using Codedown:
$ LANG=es_MX.UTF-8 \
scala-cli \
--scala 2.12.2 \
--compiler-plugin org.scala-lang.plugins:::scala-continuations-plugin:1.0.3 \
--dependency org.scala-lang.plugins::scala-continuations-library:1.0.3 \
-P:continuations:enable \
<(
curl https://earldouglas.com/effect-systems.md |
codedown scala --section '### Delimited continuations'
)
¿Cómo te llamas? James
¡Hola, James!