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("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!')")
Jdbc
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) => 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
<- 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)
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))
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))