Reusable, composable, reasonably priced algebras for typeful effects and composable applications.
From maven central
libraryDependencies += "de.knutwalker" %% "algebra-effect" % "0.1.0"
Algebras
only depend on scalaz-core
.
"de.knutwalker" %% "algebra-effect" % "0.1.0"
algebras.Effect
is a placeholder for the resulting effect type.
It is basically a wrapper around scalaz.Free
and provides typical combinators like map
and flatMap
.
The algebra-effect
module also contains syntax for arbitrary types that add an effect[F[_]]
method to
some A
to lift a value into an Effect
.
"de.knutwalker" %% "algebra-log" % "0.1.0"
algebras.Log
prodives an algebra for simple logging (debug
, info
, warn
, error
)
"de.knutwalker" %% "algebra-random" % "0.1.0"
algebras.Random
prodives an very simple algebra for random number generation.
The basic method is nextInt(n)
which returns an int in [0, n) – i.e. n is the upper exclusive bound.
This is conform with scala.util.Random.nextInt(Int)
There are some additional methods, like chooseInt(a, b)
which return an in in [a, b] and oneOf
which takes a variable number of A
s and returns one of these A
s.
Any method, that produces an effect using an algebra must be paramterized in a type F[_]
with a context bound for the algebra that this method uses. The return type is Effect[F, A]
where A
is the methods actual return type.
At the end of the universe, you have a composed Effect[F, A]
that you need to run.
To do so, first you have to combine all used algebras (their Ops, actually) into a super-algebra using scalaz.Coproduct
Then, you write interpreters for each algebra and combine those into an interpreter for the super-algebra.
An interpreter is a scala.~>
for the algebra op and some monad.
All interpreters must use the same result monad.
Finally, run effect.runM(interpreter)
to turn your Effect[F, A]
into an M[A]
.
Here's an example. Suppose we want to roll a dice...
import algebras._, Algebras._
import scalaz.{~>, Coproduct, Id}, Id.Id
object pureCore {
def roll[F[_]: Random]: Effect[F, Int] =
Random.chooseInt(1, 6)
def play[F[_]: Random : Log]: Effect[F, Int] = for {
_ ← Log.debug("rolling a dice...")
num ← roll
_ ← Log.debug(s"rolled a $num")
_ ← if (num == 6) Log.info("Yay! Rolled a 6!")
else ().effect[F]
} yield num
}
object edgeOfTheWorld {
type App[A] = Coproduct[RandomOp, LogOp, A]
val RandomInterpreter = new (RandomOp ~> Id) {
def apply[A](fa: RandomOp[A]): A = fa match {
case RandomOp.NextInt(n) ⇒ scala.util.Random.nextInt(n)
}
}
val LogInterpreter = new (LogOp ~> Id) {
def apply[A](fa: LogOp[A]): A = fa match {
case LogOp.Logs(msg, level, _) ⇒ println(s"[$level] $msg")
}
}
val AppInterpreter: App ~> Id = RandomInterpreter or LogInterpreter
val program: Effect[App, Int] = pureCore.play[App]
def main(args: Array[String]): Unit = {
program.runM(AppInterpreter)
}
}
There are two basic interpreters already available.
algebra-interpreter-rng
which uses NICTA/rng for the random number generation.
algebra-interpreter-slf4j
which uses slf4j (duh) to log the messages.
Using these interpreters, the example above could be rewritten as
import algebras._, Algebras._
import scalaz.{~>, Coproduct, effect}, effect.IO
object pureCore {
def roll[F[_]: Random]: Effect[F, Int] =
Random.chooseInt(1, 6)
def play[F[_]: Random : Log]: Effect[F, Int] = for {
_ ← Log.debug("rolling a dice...")
num ← roll
_ ← Log.debug(s"rolled a $num")
_ ← if (num == 6) Log.info("Yay! Rolled a 6!")
else ().effect[F]
} yield num
}
object edgeOfTheWorld {
type App[A] = Coproduct[RandomOp, LogOp, A]
val AppInterpreter: App ~> IO = random.Interpreter or log.Interpreter
val program: Effect[App, Int] = pureCore.play[App]
def main(args: Array[String]): Unit = {
program.runM(AppInterpreter).unsafePerformIO()
}
}
Based on https://www.parleys.com/tutorial/composable-application-architecture-reasonably-priced-monads
Apache 2