Bake Apps
Free, Coproduct & Types,
A sip of SI2712 & Onions
Pascal Voitot
@mandubian
Project September Inc.
Building Apps
w/ Free monads?
Why do we need that?
What leads us to that?
What else?
Episode 1
Describe
&
Conquer
Surrealism
Math
Building an App
Specifications
Business Domain
Logic Code
Execution & Resources
Representation
Representation
Representation
LambdaWorld Recap
2 Representations - 1 Host Language
Machine World
Machine-oriented
Host Language
Human World
Description
Of Business Logic
Execution
& Resources
LambdaWorld Recap
Separation of Concerns
Human World
Machine World
Execution Domain 1
Execution Domain n
DSL
written in
Host Language
Execution
& Resources
Description
Of Business Logic
LambdaWorld Recap
Better ways of expression & control
Description
Of Business Logic
Execution
& Resources
Human World
Machine World
Immutability
Ref Transparency
Monad
...
Constraints & Logic / Type
Behaviors & laws / Typeclass
Abstraction / Higher-Kind
Flow / Monad
...
LambdaWorld Recap
MATHEMATICS
Mathematics is a tool to express better
our thought
LambdaWorld Recap
Free Monads
Is a math tool
that solves
Separation
of
Concerns
LambdaWorld Recap
Episode 2
Cook AND Develop
Eat
Junk Food isn’t good for you
Eat Quality Food
Quality means Knowledge
Study & Search all the time
Know a bit
Practice a lot
Tradition & Science
Knowledge by Tradition
Tradition & Experiments
Tradition doesn’t know why
Science knows why (most of times)
Simplicity
Sharpness
Sureness
ENJOY
SHARE
Developing is
the same
LambdaWorld Recap
Conclusion
LambdaWorld Recap
Programming & Math = Osmosis
Free Monads
Is a math tool
that enables
Separation
of
Concerns
LambdaWorld Recap
Ce ne serait pas un peu de la M…. ?
Specifications : BioCassoulet
object BioCassoulet {
def program(clientId: String, nbCans: Int) = for {
cans <- CanStock.Check(nbCans)
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"${cans.size} in stock, missing ${nb}")
bean <- GMOCorp.OrderBeans(nb)
meat <- GMOCorp.OrderMeat(nb)
fat <- ChemTrust.OrderFat(nb * 10)
e2xx <- ChemTrust.OrderPreservatives(nb * 50)
cassoulet <- NuclearCooker.CookCassoulet(meat, bean, fat, e2xx)
newCans <- CanMachine.Make(cassoulet)
} yield (cans ++ newCans)
} else {
cans
}
_ <- DeliveryService.Send(clientId, cans)
} yield (cans)
}
1/ Monadic Logic
object BioCassoulet {
def program(clientId: String, nbCans: Int) = for {
cans <- CanStock.Check(nbCans)
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"${cans.size} in stock, missing ${nb}")
bean <- GMOCorp.OrderBeans(nb)
meat <- GMOCorp.OrderMeat(nb)
fat <- ChemTrust.OrderFat(nb * 10)
e2xx <- ChemTrust.OrderPreservatives(nb * 50)
cassoulet <- NuclearCooker.CookCassoulet(meat, bean, fat, e2xx)
newCans <- CanMachine.Make(cassoulet)
} yield (cans ++ newCans)
} else {
cans
}
_ <- DeliveryService.Send(clientId, cans)
} yield (())
}
1/ Monadic Logic
Verb
DSL
Return Type
case class Beans(); case class Meat(); case class Fat();
case class Preservatives(); case class Can(); case class Cassoulet()
sealed trait Log[A]
object Log {
final case class Info(msg: String) extends Log[Unit]
final case class Error(msg: String) extends Log[Unit]
}
sealed trait CanStock[A]
object CanStock {
final case class Check(nb: Int) extends CanStock[List[Can]]
}
sealed trait GMOCorp[A]
object GMOCorp {
final case class OrderBeans(units: Int) extends GMOCorp[Beans]
final case class OrderMeat(units: Int) extends GMOCorp[Meat]
}
2/ Grammar
Verb
DSL
Return Type
sealed trait ChemTrust[A]
object ChemTrust {
final case class OrderFat(units: Int) extends ChemTrust[Fat]
final case class OrderPreservatives(units: Int) extends ChemTrust[Preservatives]
}
sealed trait NuclearCooker[A]
object NuclearCooker {
final case class CookCassoulet(meat: Meat, beans: Beans, fat: Fat, prez: Preservatives) extends NuclearCooker[Cassoulet]
}
sealed trait CanMachine[A]
object CanMachine {
final case class Make(cassoulet: Cassoulet) extends CanMachine[List[Can]]
}�sealed trait DeliveryService[A]
object DeliveryService {
final case class Send(clientId: String, cans: List[Can]) extends DeliveryService[Unit]
}
2/ Grammar
[error] /freek-sample/src/main/scala/ScalaIO.scala:57: value map is not a member of cassoulet.CanStock.Check
[error] nbCan <- CanStock.Check(nbCans)
[error]
I’d prefer some Monads with fava beans!
Hopefully, I have some Free Coyo in the fridge!
F[A]
=> Coyoneda[F, A] : Functor (map)
=> Free[F, A] : Monad (flatMap)
F[A]
=> Coyoneda[F, A] : Functor
=> Free[F, A] : Monad
// ARTIFICIAL CONSTRUCTION TO
// DELAY THE NEED OF MONADS
object BioCassoulet {
def program(clientId: String, nbCans: Int) = for {
cans <- CanStock.Check(nbCans) .liftFC
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"Missing $nb boxes") .liftFC
bean <- GMOCorp.OrderBeans(nbCans) .liftFC
meat <- GMOCorp.OrderMeat(nbCans) .liftFC
fat <- ChemTrust.OrderFat(nbCans * 10) .liftFC
prez <- ChemTrust.OrderPreservatives(nbCans * 50).liftFC
cassoulet <- NuclearCooker.CookCassoulet(
bean, meat, fat, prez) .liftFC
newCans <- CanMachine.Make(cassoulet) .liftFC
} yield (cans ++ newCans)
} else {
Free.pure(cans)
}
_ <- DeliveryService.Send(cans) .liftFC
} yield (())
}
object BioCassoulet {
def program(clientId: String, nbCans: Int) = for {
cans <- CanStock.Check(nbCans).liftFC //=> Free[CanStock, List[Can]]
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"Missing $nb boxes").liftFC //=> Free[Log, Unit]
bean <- GMOCorp.OrderBeans(nbCans).liftFC //=> Free[GMOCorp, Beans]
meat <- GMOCorp.OrderMeat(nbCans).liftFC //=> Free[GMOCorp, Meat]
fat <- ChemTrust.OrderFat(nbCans * 10).liftFC //=> Free[ChemTrust, Fat]
prez <- ChemTrust.OrderPreservatives(nbCans * 50).liftFC
cassoulet <- NuclearCooker.CookCassoulet(bean, meat, fat, prez).liftFC
newCans <- CanMachine.Make(cassoulet).liftFC
} yield (cans ++ newCans)
} else {
Free.pure(cans)
}
_ <- DeliveryService.Send(cans).liftFC
} yield (())
}
Free[CanStock, ?]
Free[Log, ?]
Free[GMOCorp, ?]
Free[ChemTrust, ?]
Free[NuclearService, ?]
Free[CanMachine, ?]
Free[DeliveryService, ?]
Free[
CanStock OR
Log OR
GMOCorp OR
ChemTrust OR
NuclearService OR
CanMachine OR
DeliveryService OR
, ?]
type PRG =
Log OR
CanStock OR
GMOCorp OR
ChemTrust OR
NuclearCooker OR
CanMachine OR
DeliveryService
FREEK
IT
type PRG =
Log :|:
CanStock :|:
GMOCorp :|:
ChemTrust :|:
NuclearCooker :|:
CanMachine :|:
DeliveryService :|:
NilDSL
/*Artificial value to convince scalac about types*/
val PRG = DSL.Make[PRG]
def program(clientId: String, nbCans: Int) /*: Free[PRG.Cop, List[Can]] */ = for {
cans <- CanStock.Check(nbCans) .freek[PRG]
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"${cans.size} in stock, missing $nb").freek[PRG]
bean <- GMOCorp.OrderBeans(nbCans) .freek[PRG]
meat <- GMOCorp.OrderMeat(nbCans) .freek[PRG]
fat <- ChemTrust.OrderFat(nbCans * 10) .freek[PRG]
e2xx <- ChemTrust.OrderPreservatives(nbCans * 50) .freek[PRG]
cassoulet <- NuclearCooker.CookCassoulet(meat,bean,fat,e2xx).freek[PRG]
newCans <- CanMachine.Make(cassoulet) .freek[PRG]
} yield (cans ++ newCans)
} else {
Free.pure[PRG.Cop, List[Can]](cans)
}
_ <- DeliveryService.Send(clientId, cans) .freek[PRG]
} yield (cans)
Freek
Use Free programs to describe your logic & separate description from execution
Use existing Free (cats)
Helpers to Combine DSL (HigherKind CoproductK) & hide type boilerplate
Interpret Free[DSL, ?] to runtime
DSL
Monadic
Domain of Execution
M
FS2 Task
Monix Task
Scalaz Task
(even Future)
...
Machine Language
Interpret/foldMap
Compile
Natural
Transformation
DSL[_] ~> M[_]
Compiler
Scalac
Note: DSL can be translated into an effect but not necessarily
val interpreter: DSL ~> M = ???
val freeDSL: Free[DSL, A] = ???
val ma: M[A] = freeDSL.foldMap(interpreter)
val Log2Id = new (Log ~> Id) {
def apply[A](l: Log[A]): Id[A] = l match {
case Log.Info(msg) => println(s"[info] $msg")
case Log.Error(msg) => println(s"[error] $msg")
}
}
import fs2.Task
val Log2Task = new (Log ~> Task) {
def apply[A](l: Log[A]): Task[A] = l match {
case Log.Info(msg) => Task.now(println(s"[info] $msg"))
case Log.Error(msg) => Task.now(println(s"[error] $msg"))
}
}
// Postgres Interpreter
class CanPostgresRepository {
def check(nb: Int): Task[List[Can]] = ???
}
class CanStock2Task(
repo: CanPostgresRepository
) extends (CanStock ~> Task) {
def apply[A](l: CanStock[A]): Task[A] = l match {
case CanStock.Check(nb) => repo.check(nb)
}
}
val canPostgresRepo: CanPostgresRepository = ???
val canStock2Task = new CanStock2Task(canPostgresRepo)
// Mock�object CanStockAlwaysInStock extends (CanStock ~> Id) {
def mockCans(nb: Int): List[Can] = ???
def apply[A](l: CanStock[A]): Id[A] = l match {
case CanStock.Check(nb) => mockCans(nb)
}
}
When there is for 1, there is for N
Combining DSL & Interpreters
Log
Monad M
CanStock
OR
Log ~> M
CanStock ~> M
AND
GMOCorp
OR
ChemTrust
OR
NuclearCooker
OR
CanMachine
OR
GMOCorp ~> M
AND
ChemTrust ~> M
AND
NuclearCooker ~> M
AND
CanMachine ~> M
AND
interpret
Coproduct
Product
PRG.Cop
PRG.Cop ~> M
DeliverService
DeliverService ~> M
OR
AND
val Log2Task: Log ~> Task = ???
val CanStock2Task: CanStock ~> Task = ???
val GMOCorp2Task: GMOCorp ~> Task = ???
val ChemTrust2Task: ChemTrust ~> Task = ???
val NuclearCooker2Task: NuclearCooker ~> Task = ???
val CanMachine2Task: CanMachine ~> Task = ???
val DeliveryService2Task: DeliveryService ~> Task = ???
val interpreters /*:PRG.Cop ~> Task */ =
Log2Task :&:
CanStock2Task :&:
GMOCorp2Task :&:
ChemTrust2Task :&:
NuclearCooker2Task :&:
CanMachine2Task :&:
DeliveryService2Task
val nb: Int = ???
val clientId: String = ???
val program = BioCassoulet.program(clientId, nb)
val res: Task[List[Can]] = program.interpret(interpreters)
// REST OF
// CODE IS
// BORING
Free project
1 compile unit
grammar.scala
+
freeprogram.scala
1 compile unit
interpreters.scala
1 compile unit
run.scala
val interpreters =
//Log2Task :&:
CanStock2Task :&:
GMOCorp2Task :&:
ChemTrust2Task :&:
NuclearCooker2Task :&:
CanMachine2Task :&:
DeliveryService2Task
[info] Compiling 1 Scala source to /freek-sample/target/scala-2.11/classes...
[error] /freek-sample/src/main/scala/ScalaIO.scala:203: could not find implicit value for parameter sub: freek.SubCop[[A]freek.AppendK[[β]freek.In1[cassoulet.Log,β],[γ]freek.AppendK[[A]freek.In3[cassoulet.CanStock,cassoulet.GMOCorp,cassoulet.ChemTrust,A],[A]freek.In3[cassoulet.NuclearCooker,cassoulet.CanMachine,cassoulet.DeliveryService,A],γ],A],[A]freek.AppendK[[δ]freek.In3[cassoulet.CanStock,cassoulet.GMOCorp,cassoulet.ChemTrust,δ],[A]freek.In3[cassoulet.NuclearCooker,cassoulet.CanMachine,cassoulet.DeliveryService,A],A]]
[error] val res: Task[List[Can]] = program.interpret(interpreters)
Scalac & your compile errors, I still have a few fava in the fridge
I should have taken more care on return types
trait OrderError
sealed trait GMOCorp[A]
object GMOCorp {
final case class OrderBeans(units: Int) extends GMOCorp[Xor[OrderError, Beans]]
final case class OrderMeat(units: Int) extends GMOCorp[Xor[OrderError, Meat]]
}
�sealed trait ChemTrust[A]
object ChemTrust {
final case class OrderFat(units: Int) extends ChemTrust[Xor[OrderError, Fat]]
final case class OrderPreservatives(units: Int) extends ChemTrust[Xor[OrderError, Preservatives]]
}
�sealed trait NuclearCooker[A]
object NuclearCooker {
final case class CookCassoulet(meat: Meat, beans: Beans, fat: Fat, prez: Preservatives) extends NuclearCooker[Option[Cassoulet]]
}
�
trait DeliveryError
�sealed trait DeliveryService[A]
object DeliveryService {
final case class Send(clientId: String, cans: List[Can]) extends DeliveryService[Xor[DeliveryError, Unit]]
}
[error] /freek-sample/src/main/scala/ScalaIO2.scala:149: type mismatch;
[error] found : cats.data.Xor[cassoulet2.OrderError,cassoulet2.Meat]
[error] required: cassoulet2.Meat
[error] cassoulet <- NuclearCooker.CookCassoulet(meat, bean, fat, e2xx).freek[PRG]
[error] ^
[error] /freek-sample/src/main/scala/ScalaIO2.scala:149: type mismatch;
[error] found : cats.data.Xor[cassoulet2.OrderError,cassoulet2.Beans]
[error] required: cassoulet2.Beans
[error] cassoulet <- NuclearCooker.CookCassoulet(meat, bean, fat, e2xx).freek[PRG]
[error]
Classic: Monad Transformers?
OptionT, XorT, EitherT,
ListT (not in cats)
...
Freek Onion
Stacks of Traversable Monads
Controlled by Scala path-dependent types
Helpers to manipulate
Level in the stack
Anatomy of an Onion
Bulb
Bulb
Bulb
Bulb
Xor[OrderError, ?]
Option
type O =
Xor[DeliveryError, ?] :&:
Option :&:
Xor[OrderError, ?] :&:
Bulb
Monad
Traversable
Xor[DeliveryError, ?]
Xor[DeliveryError, Option[Xor[OrderError, ?]]]
Onion lifting
Option[A]
=> Option[Xor[OrderError, A]] (Monad/Traversable)
=> Xor[DeliveryError, Option[Xor[OrderError, A]]] (Monad)
Free + Onion
type PRG = Log :|: CanStock :|: GMOCorp :|: ChemTrust :|: NuclearCooker :|: CanMachine :|: DeliveryService :|: NilDSL
val PRG = DSL.Make[PRG]
�
import cats.instances.option._
type O = Xor[DeliveryError, ?] :&:
Option :&:
Xor[OrderError, ?] :&:
Bulb
// Free[PRG.Cop, Xor[DeliveryError, Option[Xor[OrderError, List[Can]]]]]
def program(clientId: String, nbCans: Int) /*: Free[PRG.Cop, O#Layers[List[Can]]]*/ = for {
cans <- CanStock.Check(nbCans).freek[PRG].onion[O]
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"...").freek[PRG].onionT[O]
bean <- GMOCorp.OrderBeans(nb).freek[PRG].onionT[O]
meat <- GMOCorp.OrderMeat(nb).freek[PRG].onionT[O]
fat <- ChemTrust.OrderFat(nb * 10).freek[PRG].onionT[O]
e2xx <- ChemTrust.OrderPreservatives(nb * 50).freek[PRG].onionT[O]
cassoulet <- NuclearCooker.CookCassoulet(...).freek[PRG].onionT[O]
newCans <- CanMachine.Make(cassoulet).freek[PRG].onion[O]
} yield (cans ++ newCans)
} else Free.pure[PRG.Cop, List[Can]](cans).onion[O]
_ <- DeliveryService.Send(clientId, cans).freek[PRG].onionT[O]
} yield (cans).value
Freek Status
v0.6.1 for cats (0.7.0)
Used in production at Project September
Requires SI2712 patch
Scala 2.11.9 / 2.12 - Typelevel Scala
Kind projector is welcome
Free Reminders
Free is just a trick to delay need of a “true” Monad
Still targets (single) monadic execution domain
Limits
Compiling Duration when combining lots of DSL
Garbage Collection can be an issue for intensive calls or recursive Free programs
Limits
Limits not so critical in real-life
Free/Cop Wrapping cost << IO cost
Just be careful if you need ultra high-perf
Cost of Free
def doit1(): Future[Unit] = Future.successful(())
def doit2(): Future[Unit] = Future.successful(())
def doit3(): Future[Unit] = Future.successful(())
@Benchmark
def run() = {
Await.result(for {
_ <- doit1()
_ <- doit2()
_ <- doit3()
} yield (()), 2.minutes)
}
def prg: Free[PRG.Cop, Unit] = for {
_ <- Doit1F.freek[PRG]
_ <- Doit2F.freek[PRG]
_ <- Doit3F.freek[PRG]
} yield (())
val doit3? = new (Doit3? ~> Future) {
def apply[A](d: Doit3?[A]) = d match {
case Doit3? => Future.successful(())
}
}
val interpreter = doit1I :&: doit2I :&: doit3I
@Benchmark
def run() = Await.result(DoitF.prg.interpret(interpreter), 2.minutes)
# JMH
# Run complete. Total time: 00:00:30
FUTURE FREE
Benchmark Score Score Units
Main.run 65931.004 30271.182 ops/s
Main.run:·gc.alloc.rate 54.343 148.796 MB/sec
Main.run:·gc.alloc.rate.norm 863.971 5155.181 B/op
Main.run:·gc.churn.PS_Eden_Space 55.574 150.883 MB/sec
Main.run:·gc.churn.PS_Eden_Space.norm 896.169 5213.585 B/op
Main.run:·gc.churn.PS_Survivor_Space 0.036 0.122 MB/sec
Main.run:·gc.churn.PS_Survivor_Space.norm 0.600 4.277 B/op
Main.run:·gc.count 23.000 93.000 counts
Main.run:·gc.time 12.000 52.000 ms
Huge Free heats the Planet
var i = 0; var f = DoitF.prg
while(i < 10000000) {
f = f.flatMap { _ => DoitF.prg }
i = i + 1;
}
println(“launch”)
f.interpret(interpreter)
}
// “Launch” appears fast (3/4 sec)
// Most of time (>2mn) is spent in interpreting
Runtime Optimizations
Other Free representations
Eliminate Free structures at compile-time (macro, plugin)…
Compile Optim
Scalac implicit resolution
by induction
is not optimized
at all
Improve Structure for Compiling
CoproductK B-Tree structure (in freek v0.6.0)
F1[_]
F2[_]
F3[_]
F4[_]
AppendK
F1[_] | F2[_] | F3[_]
F4[_]
Reduce height
from n to log(n)
Improve Structure for Compiling
Freek v0.4.3
13 DSL => 20s
20 DSL => never-ends
Freek v0.6.1
13 DSL => 4s
20 DSL => 9s
I’ll solve scalac implicit induction
soon!!!
Future?
Free allows is a big plus to better express our intents
Free requires boilerplate in host language
Free is a step to something further
Other models (MTL, finally tagless, object algebra, van laarhoven etc...)
Effect-oriented models (FreerMonad, ScalaEff, Haskell extensible effects, Idris Eff, etc...)
Future?
Better support of DSLs & Free in host languages ?
Compiler Extension
Macro
Language Extension
Better host language & runtimes ?
Type-dependent, nearer Calculus-theory, …
Time Left ?
Freekit
Writing .freek[PRG]
hurts my little hands...
object program extends Freekit(PRG) {
def apply(clientId: String, nbCans: Int) = for {
cans <- CanStock.Check(nbCans)
nb = nbCans - cans.size
cans <- if(nb > 0) {
for {
_ <- Log.Info(s"Only ${cans.size} in stock, missing ${nb} boxes")
bean <- GMOCorp.OrderBeans(nb)
meat <- GMOCorp.OrderMeat(nb)
fat <- ChemTrust.OrderFat(nb * 10)
e2xx <- ChemTrust.OrderPreservatives(nb * 50)
cassoulet <- NuclearCooker.CookCassoulet(meat, bean, fat, e2xx)
newCans <- CanMachine.Make(cassoulet)
} yield (cans ++ newCans)
} else {
Free.pure[PRG.Cop, List[Can]](cans)
}
_ <- DeliveryService.Send(clientId, cans)
} yield (())
}
NEW in
0.6.1/2
Free Program Combining
object Program1 {
type PRG = ...
val PRG = DSL.Make[PRG]
def program =
for { ... } yield (...)
}
object Program2 {
type PRG = ...
val PRG = DSL.Make[PRG]
def program =
for { ... } yield (...)
}
object Program {
type PRG = Program1.PRG :||: Program2.PRG
val PRG = DSL.Make[PRG]
def program = for {
a <- Program1.program
b <- Program2.program
} yield (...)
}
val interpreter =
interpreter1 :&&: interpreter2
Free Extension
object Program1 {
sealed trait Foo1[A]
final case class Foo11(s: Int) extends Foo1[String]
sealed trait Foo2[A]
final case class Foo21(s: String) extends Foo2[Int]
type PRG = Foo1 :|: Foo2 :|: NilDSL
val PRG = DSL.Make[PRG]
val program = for {
s <- Foo11(5).freek[PRG]
i <- Foo21(s).freek[PRG]
} yield (i)
}
NEW in
0.6.1
object Program2 {
sealed trait Foo3[A]
case class Foo31[A](foo11: Foo1[A]) extends Foo3[A]
case class Foo32(i: Int) extends Foo3[String]
type PRG = Foo3 :|: Foo2 :|: NilDSL
val PRG = DSL.Make[PRG]
// CopKNat[Program.PRG.Cop] = Program.PRG.Cop ~> Program.PRG.Cop
// Program.PRG.Cop ~> Program2.PRG.Cop
val copknat = CopKNat[Program.PRG.Cop].replace(
new (Foo1 ~> Foo3) {
def apply[A](foo1: Foo1[A]): Foo3[A] = Foo31(foo1)
}
)
val program = for {
i <- Program.program.compile(copknat)
s <- Foo32(i).freek[PRG]
} yield (s)
}
NEW in
0.6.1
Free Program Transpiling
val prg: Free[DSL1 :|: DSL2 :|: DSL3 :|: FXNil] = ???
val transpiler =
CopKNat[DSL1 :|: DSL2 :|: DSL3 :|: FXNil]
.replace(DSL2 ~> Free[DSL4 :|: DSL5 :|: DSL6 :|: FXNil])
val prg2: Free[DSL1 :|: DSL4 :|: DSL5 :|: DSL6 :|: DSL3 :|: FXNil] =
prg.transpile(transpiler)
DSL1 DSL1 DSL1
Free [ DSL2 ] => Free [ Free [ DSL4 :|: DSL5 :|: DSL6 ] ] => Free [ DSL4 ]
DSL3 DSL3 DSL5
DSL6
DSL3
Free as a Precepte (CC @skaalf)
type Step[F[_], S, A] = S => (S, F[A])
type PrecepteLike[F[_], S, A] = Free[Step[F, S, ?], A]
NEW in
0.6.x
Thank you
Pascal Voitot
@mandubian
Freek github
https://github.com/
ProjectSeptemberInc/
freek