# From Monad Transformers to an 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]`

## 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))") >>
Jdbc.update("INSERT INTO MESSAGES (`LANG`, `VALUE`) VALUES ('en', 'Hello, world!')") >>
Jdbc.update("INSERT INTO MESSAGES (`LANG`, `VALUE`) VALUES ('es', '¡Hola, mundo!')")

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

``````object Main extends App:

import java.sql.DriverManager

implicit val ec: EC = EC.global

Class.forName("org.h2.Driver")

val c = 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))``````

## Demo

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

``````\$ curl -s https://earldouglas.com/posts/scala/service.md |
codedown scala |
scala-cli -q --scala 3.1.3 --dep com.h2database:h2:2.1.214 _.scala
Right(())
Right(Map(en -> Hello, world!, es -> ¡Hola, mundo!))
Left(NotFound(no message for lang=fr))``````