DSL Final Encodings and http4s What are we going to do tonight • explore final encodings • write embedded DSLs • craft beautiful web services https://github.com/markus1189/runnersparadise Markus Hauck (codecentric AG) Functional Web Services slide 3
DSL Final Encodings and http4s Before We Start: Embedded DSLs • embedded in general purpose language (host language) • separate description of program from execution • one of the core principles in FP Markus Hauck (codecentric AG) Functional Web Services slide 4
DSL Final Encodings and http4s Common Approach: Build AST sealed trait Exp case class Lit(x: Int) extends Exp case class Neg(e: Exp) extends Exp case class Add(e1: Exp, e2: Exp) extends Exp • writing a program (syntactic sugar omitted) // 50 + -(6 + 2) Add(Lit(50), Neg(Add(Lit(6), Lit(2)))) Markus Hauck (codecentric AG) Functional Web Services slide 6
DSL Final Encodings and http4s Interpretation • we captured the program into a value • able to use different interpreters def calculate(p: Exp): Int = p match { case Lit(x) => x case Neg(e) => -calculate(e) case Add(e1,e2) => calculate(e1) + calculate(e2) } def pretty(p: Exp): String = ??? def async(srv: ExpSrv)(p: Exp): Future[Int] = ??? Markus Hauck (codecentric AG) Functional Web Services slide 7
DSL Final Encodings and http4s Initial Encoding: Drawbacks • reify explicitly as AST ≈ initial encoding • pro: interpreters can be added easily • con: try to add multiplication as an addon • we don’t want to change Exp • old interpreters should work as before Markus Hauck (codecentric AG) Functional Web Services slide 8
DSL Final Encodings and http4s Alternative • what if we scratch the whole “reify as AST” step? • leads to final encodings • • no datatype • instead: write type classes • instances = interpreter • let’s define another calculator Markus Hauck (codecentric AG) Functional Web Services slide 9
DSL Final Encodings and http4s Final Encoding: Calculator sealed trait Exp[A] { def lit(x: Int): A def neg(e: A): A def add(e1: A, e2: A): A } • writing a program // 50 + -(6 + 2) def program[A](implicit E: Exp[A]): A = { import E._ // for demo add(lit(50), neg(add(lit(6), lit(2)))) } Markus Hauck (codecentric AG) Functional Web Services slide 10
DSL Final Encodings and http4s Final Encoding: Interpreter • define interpreters as instances: implicit val calc: Exp[Int] = new Exp[Int] { def lit(x: Int): Int = x def neg(e: Int): Int = -e def add(e1: Int, e2: Int): Int = e1 + e2 } Markus Hauck (codecentric AG) Functional Web Services slide 11
DSL Final Encodings and http4s Final Encoding: Running • now that we have interpreters, we can run our program: scala> program[Int] res0: Int = 42 scala> program[String] res1: String = (50 + -(6 + 2)) • (please use newtypes for instances) Markus Hauck (codecentric AG) Functional Web Services slide 13
DSL Final Encodings and http4s Final Encoding: Extension • the important part: let’s add multiplication • goal: change should not impact existing code Markus Hauck (codecentric AG) Functional Web Services slide 14
DSL Final Encodings and http4s Final Encoding: Multiplication • add new typeclass • state required operations • note: independent of the others sealed trait ExpMul[A] { def mul(e1: A, e2: A): A } Markus Hauck (codecentric AG) Functional Web Services slide 15
DSL Final Encodings and http4s Final Encoding: Extend Interpreters • before we can run this, we need to add the instances implicit val calc: ExpMul[Int] = new ExpMul[Int] { def mul(e1: Int, e2: Int): Int = e1 * e2 } implicit val pretty: ExpMul[String] = new ExpMul[String] { def mul(e1: String, e2: String): String = s"($e1 * $e2)" } Markus Hauck (codecentric AG) Functional Web Services slide 17
DSL Final Encodings and http4s Review: Final Encoding sealed trait Exp[A] { def lit(x:Int): A def neg(e:A): A def add(e1 A, e2:A): A } new Exp[Int] { def lit(x:Int) = x def neg(e: Int) -e def add(e1:Int,e2:Int) = e1 + e2 } • define typeclass for syntax • define instance for semantics • languages are independent and composable • but: no AST. . . , what about optimizations? Markus Hauck (codecentric AG) Functional Web Services slide 19
DSL Final Encodings and http4s Final Encoding: Transformations • in fact, we can still perform program transformations • example: push down negations • idea: move all “negations” into the leaves • -(1 + 2) → (-1 + -2) Markus Hauck (codecentric AG) Functional Web Services slide 20
DSL Final Encodings and http4s Final Encoding: Transformations • we have to make the required context explicit • what do we need for pushing down negations? sealed trait Ctx case object P extends Ctx case object N extends Ctx // newtype for: Ctx => A final class Push[A](val run: Ctx => A) extends AnyVal Markus Hauck (codecentric AG) Functional Web Services slide 22
DSL Final Encodings and http4s Final Encoding: Transformations def pushNeg[A](implicit E: Exp[A]): Exp[Push[A]] = new Exp[Push[A]] { def add(e1: Push[A], e2: Push[A]): Push[A] = new Push(ctx => E.add(e1.run(ctx),e2.run(ctx))) def neg(e: Push[A]): Push[A] = new Push({ case P => e.run(N) case N => e.run(P) }) def lit(x: Int): Push[A] = new Push({ case P => E.lit(x) case N => E.neg(E.lit(x)) }) } Markus Hauck (codecentric AG) Functional Web Services slide 23
DSL Final Encodings and http4s Final Encoding: Transformations def pushNeg[A](implicit E: Exp[A]): Exp[Push[A]] = new Exp[Push[A]] { def neg(e: Push[A]): Push[A] = new Push({ case P => e.run(N) case N => e.run(P) }) } Markus Hauck (codecentric AG) Functional Web Services slide 23
DSL Final Encodings and http4s Final Encoding: Transformations def pushNeg[A](implicit E: Exp[A]): Exp[Push[A]] = new Exp[Push[A]] { def lit(x: Int): Push[A] = new Push({ case P => E.lit(x) case N => E.neg(E.lit(x)) }) } Markus Hauck (codecentric AG) Functional Web Services slide 23
DSL Final Encodings and http4s Final Encoding: Transformations def pushNeg[A](implicit E: Exp[A]): Exp[Push[A]] = new Exp[Push[A]] { def add(e1: Push[A], e2: Push[A]): Push[A] = new Push(ctx => E.add(e1.run(ctx),e2.run(ctx))) def neg(e: Push[A]): Push[A] = new Push({ case P => e.run(N) case N => e.run(P) }) def lit(x: Int): Push[A] = new Push({ case P => E.lit(x) case N => E.neg(E.lit(x)) }) } Markus Hauck (codecentric AG) Functional Web Services slide 23
DSL Final Encodings and http4s Final Encoding: Review • we can do transformations after all • (although still not as nice as pattern matching) • extension is possible without adapting existing code • so far, so good Markus Hauck (codecentric AG) Functional Web Services slide 25
DSL Final Encodings and http4s http4s What is http4s A typeful, purely functional, streaming library for HTTP clients and servers in Scala. • typeful — self-documentation and compile-time verification • purely functional — promote composability and reasoning • streaming — large payloads in constant space and websockets (currently being rewritten to use cats and fs2) Markus Hauck (codecentric AG) Functional Web Services slide 28
DSL Final Encodings and http4s Writing Web Services With Http4s • define HttpService using the built-in DSL • really just Request => Task[Response] • (Task[A] is a better1 Future[A]) • define routes via pattern matching: HttpService { case req @ GET -> Root / "hello" => handleHelloWorld(req) } 1in the context of FP Markus Hauck (codecentric AG) Functional Web Services slide 29
DSL Final Encodings and http4s Our API GET "/runner/<runner-id>" POST "/runner" GET "/race/<race-id>" POST "/race" GET "/registration/<race-id>" PUT "/registration" Markus Hauck (codecentric AG) Functional Web Services slide 31
DSL Final Encodings and http4s Our API HttpService { case GET -> Root / "runner" / RunnerIdVar(v) => ??? case req @ POST -> Root / "runner" => ??? case GET -> Root / "race" / RaceIdVar(v) => ??? case req @ POST -> Root / "race" => ??? case GET -> Root / "registration" / RaceIdVar(v) => ??? case req @ PUT -> Root / "registration" => ??? } Markus Hauck (codecentric AG) Functional Web Services slide 32
DSL Final Encodings and http4s The Idea • define a web service that uses a DSL • each request modeled as a program that is executed Markus Hauck (codecentric AG) Functional Web Services slide 33
DSL Final Encodings and http4s The Runners Paradise DSL trait RunnerAlg[F[_]] { def saveRunner(runner: Runner): F[Unit] def findRunner(id: RunnerId): F[Option[Runner]] } • we use a higher-kinded type F • kind: * -> *, i.e. it needs another type of kind * • results are wrapped in F[ ] • instances choose the concrete F Markus Hauck (codecentric AG) Functional Web Services slide 34
DSL Final Encodings and http4s Let’s hammer • each route leads to a program being executed • the interpreter is left abstract, instantiate when running • let’s implement: registration of a runner for a race PUT "/registration" Markus Hauck (codecentric AG) Functional Web Services slide 36
DSL Final Encodings and http4s Registration Program def registerOpt[F[_]: // still abstract Monad: // flatMap RunnerAlg: // runners RaceAlg: // races RegistrationAlg]( // registrations runnerId: RunnerId, // runner id raceId: RaceId // race id ): F[Option[Registration]] // wrapped in F Markus Hauck (codecentric AG) Functional Web Services slide 39
DSL Final Encodings and http4s Getting More Concrete: Instances • to actually run programs, we need to have instances • need to satisfy all constraints • pure in-memory, cassandra, postgres, redis, . . . Markus Hauck (codecentric AG) Functional Web Services slide 40
DSL Final Encodings and http4s Instances for our DSL class Cass[A]( val value: ReaderT[Task, RunnersParadiseDb, A] ) extends AnyVal { def run: RunnersParadiseDb => Task[A] = value.run } • RunnersParadiseDb => Task[A] • given a connection to the DB, asynchronous results Markus Hauck (codecentric AG) Functional Web Services slide 41
DSL Final Encodings and http4s Putting It Together • remember signature of routes? Request => Task[Response] • at the end our program has to produce a Task • but we don’t want to be limited by this Markus Hauck (codecentric AG) Functional Web Services slide 43
DSL Final Encodings and http4s So what? • okay we added a LOT of abstraction in our web service. . . • why should we do that? • composability • testability Markus Hauck (codecentric AG) Functional Web Services slide 46
DSL Final Encodings and http4s Composability • we can choose interpreters and also mix them as we want: • remember push down negations? • logging • security • timeouts • tracing Markus Hauck (codecentric AG) Functional Web Services slide 47
DSL Final Encodings and http4s Testability • use in-memory interpreter by only changing the type parameter • formulate laws on our type classes • use ScalaCheck to check for all instances (Cassandra, Redis, . . . ) • nice separation of domain logic that should not use the DSL Markus Hauck (codecentric AG) Functional Web Services slide 48
DSL Final Encodings and http4s Review of Final Encodings • initial encoding: pattern matching and transformations more obvious • final encoding: easier to extend, same expressiveness • btw: nice way to solve expression problem (exercise) • you can always convert between initial and final Markus Hauck (codecentric AG) Functional Web Services slide 49
DSL Final Encodings and http4s Final Encodings and Free Monads • final encoding + higher kinded type constructor. . . • similar to Free Monads, the latter use an initial encoding • the two are interchangeable • final encoding → free monads: type class instance that builds the free structure • free monads → final encoding: take in an instance and use it Markus Hauck (codecentric AG) Functional Web Services slide 50
DSL Final Encodings and http4s Finalizing a Free Monad Program def finalize[R, F[_]:Monad:RunnerAlg]( p: FreeC[RunnerAlgFree, R]): F[R] = Free.runFC(p)(new (RunnerAlgFree ~> F) { def apply[A](fa: RunnerAlgFree[A]): F[A] = fa match { case SaveRunner(runner) => RunnerAlg[F].saveRunner(runner) case FindRunner(id) => RunnerAlg[F].findRunner(id) }}) Markus Hauck (codecentric AG) Functional Web Services slide 51
DSL Final Encodings and http4s Free Monads vs Final Encoding • being able to convert is great • final encoding: easy to compose many languages, lower overhead • free monads: reify as first class values, pattern matching Markus Hauck (codecentric AG) Functional Web Services slide 52
DSL Final Encodings and http4s Further References • Oleg Kiselyov: Lecture notes on Typed Tagless Final Interpreters • Runner’s Paradise Code: https://github.com/markus1189/runnersparadise • http4s: http://http4s.org/ Markus Hauck (codecentric AG) Functional Web Services slide 53