Effect Systems

January 24, 2014

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:

Programs as Values

Imagine a pure functional program that looks something like the following:

val enProgram =
  for
    _    <- write("What's your name? ")
    name <- readLn()
    _    <- write(s"Hello, ${name}!\n")
  yield ()

val esProgram =
  for
    _    <- write("¿Cómo te llamas? ")
    name <- readLn()
    _    <- write(s"¡Hola, ${name}!\n")
  yield ()

val program =
  for
    lang <- readEnv("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.

Table of contents


Monad transformers

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.

Monad transformers in Scala 2

Monad

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)
}

ID

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)
  }

ReadEnvT

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] =
          F.pure(x)
      }

    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] =
          F.flatMap(F.map(x.runEnv(env))(f))(_.runEnv(env))
      }
  }

ReadLnT

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] =
          F.pure(x)
      }

    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] =
          F.flatMap(F.map(x.runIn(readLn))(f))(_.runIn(readLn))
      }
  }

WriteT

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] =
          F.pure(x)
      }

    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] =
          F.flatMap(F.map(x.runOut(write))(f))(_.runOut(write))
      }
  }

Usage

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? ")
    name <- readLn()
    _    <- write(s"Hello, ${name}!\n")
  } yield ()

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

val program: Program[Unit] =
  for {
    lang <- readEnv("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)

Try it out

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!

Monad transformers in Scala 2 with Kind Projector

Monad

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)
}

ID

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)
  }

ReadEnvT

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] =
          F.pure(x)
      }

    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] =
          F.flatMap(F.map(x.runEnv(env))(f))(_.runEnv(env))
      }
  }

ReadLnT

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] =
          F.pure(x)
      }

    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] =
          F.flatMap(F.map(x.runIn(readLn))(f))(_.runIn(readLn))
      }
  }

WriteT

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] =
          F.pure(x)
      }

    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] =
          F.flatMap(F.map(x.runOut(write))(f))(_.runOut(write))
      }
  }

Usage

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? ")
    name <- readLn()
    _    <- write(s"Hello, ${name}!\n")
  } yield ()

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

val program: Program[Unit] =
  for {
    lang <- readEnv("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)

Try it out

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!

Monad transformers in Scala 3

Monad

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)

ID

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)

ReadEnvT

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] =
          F.pure(x)

    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] =
          F.flatMap(F.map(x.runEnv(env))(f))(_.runEnv(env))

ReadLnT

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] =
          F.pure(x)

    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] =
          F.flatMap(F.map(x.runIn(readLn))(f))(_.runIn(readLn))

WriteT

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] =
          F.pure(x)

    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] =
          F.flatMap(F.map(x.runOut(write))(f))(_.runOut(write))

Usage

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? ")
    name <- readLn()
    _    <- write(s"Hello, ${name}!\n")
  yield ()

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

val program: Program[Unit] =
  for
    lang <- readEnv("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)

Try it out

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!

Stacked monad transformers in practice

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:

Let:

Effect

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)))

JDBC services

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]] =
    c.synchronized {
      val ac = c.getAutoCommit
      c.setAutoCommit(false)
      val ea =
        s.unsafeRun(c).map {
          case r @ Right(_) =>
            c.commit()
            r
          case l @ Left(_) =>
            c.rollback()
            l
        }
      c.setAutoCommit(ac)
      ea
    }

  def unsafeRun[A](s: Jdbc[A], c: Connection)(implicit ec: EC): Either[JdbcError, A] =
    Await.result(Jdbc.transact(s, c), Duration(100, "millis"))

  def update(u: String)(implicit ec: EC): Jdbc[Unit] =
    Effect(c =>
      Future(
        Try {
          val s = c.createStatement()
          s.executeUpdate(u)
          s.close()
        } 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))
        }
      )
    )

Example

object MyDatabase:

  import Jdbc.Jdbc

  def init(implicit ec: EC): Jdbc[Unit] =
    Jdbc.update(
      """|CREATE TABLE MESSAGES (
         |  `LANG` VARCHAR(5),
         |  `VALUE` VARCHAR(128)
         |)""".stripMargin
    ) >>
    Jdbc.update(
      """|INSERT INTO MESSAGES (`LANG`, `VALUE`)
         | VALUES ('en', 'Hello, world!')
         |""".stripMargin
    ) >>
    Jdbc.update(
      """|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)
        s.setString(1, lang)
        val r = s.executeQuery()
        val vo = if (r.next()) then Some(r.getString("VALUE")) else None
        s.close()
        vo
      )
      .flatMap {
        case Some(x) =>
          Effect.success(x)
        case None =>
          Effect.failure(NotFound(s"no message for lang=${lang}"))
      }
object MyProgram:

  import Jdbc.Jdbc

  def succeeds(implicit ec: EC): Jdbc[Map[String, String]] =
    for
      en <- MyDatabase.get("en")
      es <- MyDatabase.get("es")
    yield Map("en" -> en, "es" -> es)

  def fails(implicit ec: EC): Jdbc[Map[String, String]] =
    for
      fr <- MyDatabase.get("fr")
      es <- MyDatabase.get("es")
    yield Map("fr" -> fr, "es" -> es)

Usage

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))

Try it out

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

Free monads are the defunctionalization of tagless-final.

Free monads in Scala 3

Natural transformation

trait ~>[-F[_], +G[_]]:
  def apply[A](f: F[A]): G[A]

Monad

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]

ID

type ID[A] = A

given 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)

Free monad

enum Free[F[_], A]:

  import Free._

  def map[B](f: A => B): Free[F, B] =
    flatMap { a => pure(f(a)) }

  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) =>
        summon[Monad[G]].pure(a)
      case Suspend(fa) =>
        nt(fa)
      case Bind(fa, f) =>
        val mg = summon[Monad[G]]
        val ga = fa.foldMap(nt)
        mg.flatMap(ga)(f(_).foldMap(nt))

  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)

Usage

enum EnvAlgebra[A]:
  case ReadEnv(name: String) extends EnvAlgebra[String]

enum StdioAlgebra[A]:
  case 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? "))
    name <- Free.liftM(StdioAlgebra.ReadLn)
    _    <- Free.liftM(StdioAlgebra.Write(s"Hello, ${name}!\n"))
  yield ()

val esProgram: Free[StdioAlgebra, Unit] =
  for
    _    <- Free.liftM(StdioAlgebra.Write("¿Cómo te llamas? "))
    name <- Free.liftM(StdioAlgebra.ReadLn)
    _    <- Free.liftM(StdioAlgebra.Write(s"¡Hola, ${name}!\n"))
  yield ()

val program: Free[[A] =>> StdioAlgebra[A] | EnvAlgebra[A], Unit] =
  for
    lang <- Free.liftM(EnvAlgebra.ReadEnv("LANG"))
    _    <- if (lang.startsWith("es")) then esProgram
            else enProgram
  yield ()
program.foldMap[ID] {
  new ~>[[A] =>> StdioAlgebra[A] | EnvAlgebra[A], ID]:
    def apply[A](x: StdioAlgebra[A] | EnvAlgebra[A]): ID[A] =
      x match
        case EnvAlgebra.ReadEnv(name)   => sys.env(name)
        case StdioAlgebra.ReadLn        => scala.io.StdIn.readLine()
        case StdioAlgebra.Write(output) => print(output)
}

Try it out

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!

References


Tagless-final

Tagless-final is the Church encoding of free monads.

Tagless-final in Scala 3

Monad

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]

extension [M[_]: Monad, A](ma: M[A])
  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)

ID

type ID[A] = A

given 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)

Usage

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? ")
    name <- summon[StdioAlgebra[F]].readLn
    _    <- 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? ")
    name <- summon[StdioAlgebra[F]].readLn
    _    <- summon[StdioAlgebra[F]].write(s"¡Hola, ${name}!\n")
  yield ()

def program[F[_]: Monad: StdioAlgebra: EnvAlgebra]: F[Unit] =
  for
    lang <- summon[EnvAlgebra[F]].readEnv("LANG")
    _    <- if (lang.startsWith("es")) then esProgram()
            else enProgram()
  yield ()
given liveEnv: EnvAlgebra[ID] =
  new EnvAlgebra[ID]:
    override def readEnv(name: String): ID[String] = sys.env(name)

given liveStdio: StdioAlgebra[ID] =
  new StdioAlgebra[ID]:
    override def readLn: ID[String] = scala.io.StdIn.readLine()
    override def write(output: String): ID[Unit] = print(output)

program

Try it out

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!

Tagless-final via Cats

Usage

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? ")
    name <- summon[StdioAlgebra[F]].readLn
    _    <- 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? ")
    name <- summon[StdioAlgebra[F]].readLn
    _    <- summon[StdioAlgebra[F]].write(s"¡Hola, ${name}!\n")
  yield ()

def program[F[_]: Monad: StdioAlgebra: EnvAlgebra]: F[Unit] =
  for
    lang <- summon[EnvAlgebra[F]].readEnv("LANG")
    _    <- if (lang.startsWith("es")) then esProgram()
            else enProgram()
  yield ()
given liveEnv: EnvAlgebra[Id] =
  new EnvAlgebra[Id]:
    override def readEnv(name: String): Id[String] = sys.env(name)

given liveStdio: StdioAlgebra[Id] =
  new StdioAlgebra[Id]:
    override def readLn: Id[String] = scala.io.StdIn.readLine()
    override def write(output: String): Id[Unit] = print(output)

program[Id]

Try it out

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!

References


Reader monad

Reader monad in Scala 2

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)
  }
}

Partial Injection (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)

Try it out

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!

Reader monad via ZIO

Usage

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

def readEnv(name: String): zio.ZIO[HasEnv, Throwable, String] =
  zio.ZIO.environmentWithZIO { r =>
    zio.ZIO.attempt {
      r.get.env(name)
    }
  }
trait HasReadLn {
  def readLn(): String
}

def readLn: zio.ZIO[HasReadLn, Throwable, String] =
  zio.ZIO.environmentWithZIO { r =>
    zio.ZIO.attempt {
      r.get.readLn()
    }
  }
trait HasWrite {
  def write(output: String): Unit
}

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

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

def program: zio.ZIO[HasEnv with HasReadLn with HasWrite, Throwable, Unit] =
  for {
    lang <- readEnv("LANG")
    _    <- if (lang.startsWith("es")) {
              esProgram
            } else {
              enProgram
            }
  } yield ()
zio.Unsafe.unsafely {
//zio.Unsafe.unsafe { implicit u: zio.Unsafe =>
  zio.Runtime.default.unsafe.run(
    program.provide(
      zio.ZLayer.succeed(
        new HasEnv {
          override def env: Map[String, String] = sys.env
        }
      ),
      zio.ZLayer.succeed(
        new HasReadLn {
          override def readLn(): String = scala.io.StdIn.readLine()
        }
      ),
      zio.ZLayer.succeed(
        new HasWrite {
          override def write(output: String): Unit = print(output)
        }
      )
    )
  ).getOrThrowFiberFailure()
}

Try it out

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!

Reader monad in Java

Reader

import java.io.*;
import java.util.*;
import java.util.function.*;

class Reader<R, A> {

  private final Function<R, A> k;

  A run(final R r) {
    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) -> {
      k.accept(a);
      return 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);
    });
  }
}

Usage

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 =
            Optional.ofNullable(env.get("LANG"))
              .map((x) -> x.substring(0, 2))
              .orElse("en");
          switch (lang) {
            case "es": return esProgram();
            default: return enProgram();
          }
      });
  }
}

Live

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);
      }
    };
}

Test

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) {
        output.push(line);
      }
    };
}

Try it out

public class Main {
  public static void main(String[] args) {
    if ("live".equalsIgnoreCase(System.getenv("ENV"))) {
      Greeter.program().run(Live.env);
    } else {
      Greeter.program().run(Test.env);
      System.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!

Continuation-passing style

Delimited continuations

// 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] =
  shift { (k: String => Unit) =>
    ks.push({ r: HasEnv => k(r.env(name)) })
    ()
  }
trait HasReadLn {
  def readLn(): String
}

def readLn(): String@cpsParam[Unit, Unit] =
  shift { (k: String => Unit) =>
    ks.push({ r: HasReadLn => k(r.readLn()) })
    ()
  }
trait HasWrite {
  def write(output: String): Unit
}

def write(output: String): Unit@cpsParam[Unit, Unit] =
  shift { (k: Unit => Unit) =>
    ks.push({ r: HasWrite =>
      r.write(output)
      k()
    })
    ()
  }
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)
}

sbt configuration

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:

/***
scalaVersion := "2.12.2"

addCompilerPlugin( "org.scala-lang.plugins"
                 % "scala-continuations-plugin_2.12.2"
                 % "1.0.3"
                 )

libraryDependencies +=
  "org.scala-lang.plugins" %% "scala-continuations-library" % "1.0.3"

scalacOptions += "-P:continuations:enable"
*/

Try it out

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!

Freer Monads


Generalized Effect Composition