Handlers
Freestyle empowers programs whose runtime can easily be overridden via implicit evidence.
As part of its design, Freestyle is compatible with Free
and the traditional patterns around it. Apps built with Freestyle give developers the freedom to choose automatic or manual algebras, modules, and interpreters, and intermix them as you see fit in applications based on the desired encoding.
Implementation
Freestyle automatically generates an abstract definition of an interpreter for each one of the
algebras annotated with @free
.
This allows you to build the proper runtime definitions for your algebras by simply extending the Handler[M[_]]
member in your algebras companion.
Consider the following algebra adapted to Freestyle from the Typelevel Cats Free monads examples:
import freestyle.free._
// import freestyle.free._
import cats.implicits._
// import cats.implicits._
@free trait KVStore {
def put[A](key: String, value: A): FS[Unit]
def get[A](key: String): FS[Option[A]]
def delete(key: String): FS[Unit]
def update[A](key: String, f: A => A): FS.Seq[Unit] =
get[A](key).freeS flatMap {
case Some(a) => put[A](key, f(a)).freeS
case None => ().pure[FS.Seq]
}
}
// defined trait KVStore
// defined object KVStore
To define a runtime interpreter for this, we simply extend KVStore.Handler[M[_]]
and implement its abstract members:
import cats.data.State
// import cats.data.State
type KVStoreState[A] = State[Map[String, Any], A]
// defined type alias KVStoreState
implicit val kvStoreHandler: KVStore.Handler[KVStoreState] = new KVStore.Handler[KVStoreState] {
def put[A](key: String, value: A): KVStoreState[Unit] =
State.modify(_.updated(key, value))
def get[A](key: String): KVStoreState[Option[A]] =
State.inspect(_.get(key).map(_.asInstanceOf[A]))
def delete(key: String): KVStoreState[Unit] =
State.modify(_ - key)
}
// kvStoreHandler: KVStore.Handler[KVStoreState] = $anon$1@7a4cd126
As you may have noticed, instead of implementing a Natural transformation F ~> M
, we implement methods that closely resemble each one of the smart constructors in our @free
algebras in Freestyle. This is not an imposition but rather a convenience as the resulting instances are still Natural Transformations.
In the example above, KVStore.Handler[M[_]]
is already a Natural transformation of type KVStore.Op ~> KVStoreState
in which its
apply
function automatically delegates each step to the abstract method that you are implementing as part of the Handler.
Alternatively, if you would rather implement a natural transformation by hand, you can still do that by choosing not to implement
KVStore.Handler[M[_]]
and providing one like so:
import cats.~>
// import cats.$tilde$greater
implicit def manualKvStoreHandler: KVStore.Op ~> KVStoreState =
new (KVStore.Op ~> KVStoreState) {
def apply[A](fa: KVStore.Op[A]): KVStoreState[A] =
fa match {
case KVStore.PutOp(key, value) =>
State.modify(_.updated(key, value))
case KVStore.GetOp(key) =>
State.inspect(_.get(key).map(_.asInstanceOf[A]))
case KVStore.DeleteOp(key) =>
State.modify(_ - key)
}
}
// manualKvStoreHandler: KVStore.Op ~> KVStoreState
Composition
Freestyle performs automatic composition of interpreters by providing the implicit machinery necessary to derive a Module interpreter
by the evidence of it’s algebras’ interpreters.
To illustrate interpreter composition, let’s define a new algebra Log
which we will compose with our KVStore
operations:
@free trait Log {
def info(msg: String): FS[Unit]
def warn(msg: String): FS[Unit]
}
// defined trait Log
// defined object Log
Once our algebra is defined we can easily write an interpreter for it:
import cats.implicits._
// import cats.implicits._
implicit def logHandler: Log.Handler[KVStoreState] =
new Log.Handler[KVStoreState] {
def info(msg: String): KVStoreState[Unit] = println(s"INFO: $msg").pure[KVStoreState]
def warn(msg: String): KVStoreState[Unit] = println(s"WARN: $msg").pure[KVStoreState]
}
// logHandler: Log.Handler[KVStoreState]
Before we create a program combining all operations, let’s consider both KVStore
and Log
as part of a module in our application:
@module trait Backend {
val store: KVStore
val log: Log
}
// defined trait Backend
// defined object Backend
When @module
is materialized, it will automatically create the Coproduct
that matches the interpreters necessary to run the Free
structure
below:
def program[F[_]](implicit B: Backend[F]): FreeS[F, Option[Int]] = {
import B.store._, B.log._
for {
_ <- put("wild-cats", 2)
_ <- info("Added wild-cats")
_ <- update[Int]("wild-cats", (_ + 12))
_ <- info("Updated wild-cats")
_ <- put("tame-cats", 5)
n <- get[Int]("wild-cats")
_ <- delete("tame-cats")
_ <- warn("Deleted tame-cats")
} yield n
}
// program: [F[_]](implicit B: Backend[F])freestyle.free.FreeS[F,Option[Int]]
Once we have combined our algebras, we can evaluate them by providing implicit evidence of the Coproduct interpreters. import freestyle.free.implicits._
brings into scope, among others, the necessary implicit definitions to derive a unified interpreter given implicit evidence of each one of the individual algebra’s interpreters:
import freestyle.free.implicits._
// import freestyle.free.implicits._
program[Backend.Op].interpret[KVStoreState]
// res0: KVStoreState[Option[Int]] = cats.data.IndexedStateT@51bb2e39
Alternatively, you can build your interpreters by hand if you choose not to use Freestyle’s implicit machinery. This can quickly grow unruly as the number of algebras increase in an application, but it’s also possible, in the spirit of providing two-way compatibility in all areas between manually built ADTs and Natural Transformations, and the ones automatically derived by Freestyle.
Tagless Interpretation
Some imports:
import cats._
import cats.implicits._
import freestyle.tagless._
Tagless final algebras are declared using the @tagless
macro annotation.
@tagless(true) trait Validation {
def minSize(s: String, n: Int): FS[Boolean]
def hasNumber(s: String): FS[Boolean]
}
// defined trait Validation
// defined object Validation
@tagless(true) trait Interaction {
def tell(msg: String): FS[Unit]
def ask(prompt: String): FS[String]
}
// defined trait Interaction
// defined object Interaction
Once your @tagless
algebras are defined, you can start building programs that rely upon implicit evidence of those algebras
being present, for the target runtime monad you are planning to interpret to.
def taglessProgram[F[_]: Monad](implicit validation : Validation[F], interaction: Interaction[F]) =
for {
userInput <- interaction.ask("Give me something with at least 3 chars and a number on it")
valid <- (validation.minSize(userInput, 3), validation.hasNumber(userInput)).mapN(_ && _)
_ <- if (valid)
interaction.tell("awesomesauce!")
else
interaction.tell(s"$userInput is not valid")
} yield ()
// taglessProgram: [F[_]](implicit evidence$1: cats.Monad[F], implicit validation: Validation[F], implicit interaction: Interaction[F])interaction.FS[Unit]
Note that unlike in @free
, F[_]
here refers to the target runtime monad. This is to provide an allocation free model where your
ops are not being reified and then interpreted. This allocation step in Free monads is what allows them to be stack-safe.
The tagless final encoding with direct style syntax is as stack-safe as the target F[_]
you are interpreting to.
Once our @tagless
algebras are defined, we can provide Handler
instances in the same way we do with @free
.
import scala.util.Try
// import scala.util.Try
implicit val validationHandler = new Validation.Handler[Try] {
override def minSize(s: String, n: Int): Try[Boolean] = Try(s.size >= n)
override def hasNumber(s: String): Try[Boolean] = Try(s.exists(c => "0123456789".contains(c)))
}
// validationHandler: Validation.Handler[scala.util.Try] = $anon$1@20b72db7
implicit val interactionHandler = new Interaction.Handler[Try] {
override def tell(s: String): Try[Unit] = Try(println(s))
override def ask(s: String): Try[String] = Try("This could have been user input 1")
}
// interactionHandler: Interaction.Handler[scala.util.Try] = $anon$1@629c7df8
At this point, we can run our pure programs at the edge of the world.
taglessProgram[Try]
// awesomesauce!
// res1: interactionHandler.FS[Unit] = Success(())
Stack Safety
Freestyle provides two strategies to make @tagless
encoded algebras stack safe.
Interpreting to a stack safe monad
The handlers above are not stack safe because Try
is not stack-safe. Luckily, we can still execute
our program stack safe with Freestyle by interpreting to Free[Try, ?]
instead of Try
directly.
This small penalty and a few extra allocations will make our programs stack safe.
We can safely invoke our program in a stack safe way, running it first to Free[Try, ?]
then to Try
with Free#runTailRec
:
scala> import cats.free.Free
import cats.free.Free
scala> taglessProgram[Free[Try, ?]].runTailRec
awesomesauce!
res2: scala.util.Try[Unit] = Success(())
Interpreting combined @tagless
and @free
algebras
When combining @tagless
and @free
algebras, we need all algebras to be considered in the final Coproduct we are interpreting to.
We can simply use tagless’ .StackSafe
representation in modules so they are considered for the final Coproduct.
import freestyle.free._
import freestyle.free.implicits._
import freestyle.free.logging._
import freestyle.free.loggingJVM.implicits._
def taglessProgram[F[_]]
(implicit log: LoggingM[F],
validation : Validation.StackSafe[F],
interaction: Interaction.StackSafe[F]) = {
import cats.implicits._
for {
userInput <- interaction.ask("Give me something with at least 3 chars and a number on it")
valid <- (validation.minSize(userInput, 3), validation.hasNumber(userInput)).mapN(_ && _)
_ <- if (valid)
interaction.tell("awesomesauce!")
else
interaction.tell(s"$userInput is not valid")
_ <- log.debug("Program finished")
} yield ()
}
// taglessProgram: [F[_]](implicit log: freestyle.free.logging.LoggingM[F], implicit validation: Validation.StackSafe[F], implicit interaction: Interaction.StackSafe[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],Unit]
@module trait App {
val interaction: Interaction.StackSafe
val validation: Validation.StackSafe
val log: LoggingM
}
// defined trait App
// defined object App
Once all of our algebras are considered, we can execute our programs:
taglessProgram[App.Op].interpret[Try]
// <console>:57: warning: object implicits in package loggingJVM is deprecated (since 0.6.2): Use freestyle.free.loggingJVM.journal.implicits or freestyle.free.loggingJVM.log4s.implicits instead
// taglessProgram[App.Op].interpret[Try]
// ^
// awesomesauce!
// res3: scala.util.Try[Unit] = Success(())
Now that we’ve learned to define our own interpreters, let’s jump into Parallelism.