From aa14d27dd390f5478b0b37e670a99d58210f5e5e Mon Sep 17 00:00:00 2001 From: calvinlfer Date: Tue, 29 Nov 2016 01:49:35 -0500 Subject: [PATCH] Initial project is ready --- README.md | 217 ++++++++++++++++++ build.sbt | 20 ++ project/build.properties | 1 + project/plugins.sbt | 1 + src/main/resources/logback.xml | 12 + .../com/experiments/calvin/Application.scala | 23 ++ .../scala/com/experiments/calvin/Http.scala | 56 +++++ .../experiments/calvin/domain/package.scala | 28 +++ .../calvin/interpreters/package.scala | 47 ++++ 9 files changed, 405 insertions(+) create mode 100644 README.md create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 src/main/resources/logback.xml create mode 100644 src/main/scala/com/experiments/calvin/Application.scala create mode 100644 src/main/scala/com/experiments/calvin/Http.scala create mode 100644 src/main/scala/com/experiments/calvin/domain/package.scala create mode 100644 src/main/scala/com/experiments/calvin/interpreters/package.scala diff --git a/README.md b/README.md new file mode 100644 index 0000000..c84174f --- /dev/null +++ b/README.md @@ -0,0 +1,217 @@ +# Http4s + Circe + Free Monads +*In the pursuit of pure functional web applications* + +Libraries: +- [Http Server: Http4S](http://http4s.org/) +- [JSON Parser: Circe](https://github.com/circe/circe) +- [ScalaZ](https://github.com/scalaz/scalaz) + +Credits: +- [David Hoyt: Drinking the Free Kool-Aid](https://www.youtube.com/watch?v=T4956GI-6Lw) +- [Chris Myers: A year living freely](https://www.youtube.com/watch?v=rK53C-xyPWw) +- [Functional Programming in Scala](https://www.manning.com/books/functional-programming-in-scala) +- [John De Goes: A Modern Architecture for FP](http://degoes.net/articles/modern-fp) + +## Purpose +The purpose of this project was to apply the theory behind the Free Monad in a semi-practical way by trying to craft a +pure functional web application in terms of having a functional core and an imperative shell. The edges where +side-effects can occur is when you have completed processing the request and you are sending a response. This is where +we interpret the pure Free instructions using an interpreter which causes side-effects to occur in the system. + +### A very quick and dirty introduction to Free Monads +The idea behind the Free Monad is to separate the description of the computation from the execution of the computation. +Doing this allows us to keep a pure embedded DSL where we describe what we would like to do but NOT how to do it. You +can see that these instructions are *free to be interpreted* since they describe what they would like to do. The 'how' +part is the responsibility of the interpreter which interprets these free instructions and gives them meaning actually +performing the execution of the instruction which most likely causes side effects (reading from a database, logging, +etc.) + +I have not used Monad Coproducts that Runar suggests to combine the different types of instructions together. Instead, +I have gone for a very simple approach similar to the one showed by Chris Myers although the way I have done it does +suffer from type erasure. + +The free instructions are defined like this: +```scala + import scalaz.Free + + // general application instruction + sealed trait AppInstruction[Result] { + def lift: Free[AppInstruction, Result] = Free.liftF(this) + } + + // more specific types of instructions + sealed trait LogInstruction[Result] + sealed trait DatabaseInstruction[Result] + + // AST + case class GetById(id: String) extends AppInstruction[Option[Person]] with DatabaseInstruction[Option[Person]] + case class Save(person: Person) extends AppInstruction[Unit] with DatabaseInstruction[Unit] + case class Log(info: String) extends AppInstruction[Unit] with LogInstruction[Unit] + + // data that represents a person + case class Person(username: String, firstName: String, lastName: String) +``` + +I have two types of instructions, `LogInstruction` for log related concerns and `DatabaseInstruction` for persistence +related concerns. The descriptions of the computations are: +- `GetById` - I want to get a person by their id from the database +- `Save` - I want to save a new Person to the database +- `Log` - I want to log information + +Notice that their results are encoded in the `AppInstruction` for example for `GetById`, we want to get back a Person +provided a Person with that id is present so the result is a `Option[Person]` encoded in the `AppInstruction` + +We provide smart constructors to lift these instructions into `Free` so we get a Monad for free. This gives us the +ability to sequence our instructions (log and then save or get-by-id and then log and then save) + +So far, we have covered the description of the instructions. We haven't covered how to give meaning/interpret these +instructions/descriptions. This is where the interpreter comes into play. The interpreter is responsible for giving +meaning to the instructions and quite possible performing side-effects as a result of doing so. + +#### What does an Interpreter for Free instructions look like? +The interpreter is a [Natural Transformation typeclass](http://eed3si9n.com/learning-scalaz/Natural-Transformation.html) +that abstracts over higher-kinded-types/functions-at-the-type-level/type-constructors (think List, Future, Option). A +Natural transformation provides a conversion from one higher kinded type to another higher-kinded-type whilst +preserving the internal structure. + +The Natural Transformation typeclass more or less looks like this: +```scala +trait NaturalTransformation[F[_], G[_]] { + def apply[A](given: F[A]): G[A] +} + +// type alias for ~> sugar +type ~>[F[_], G[_]] = NaturalTransformation[F, G] +``` + +I said that it provides a conversion from one higher-kinded-type to another. A simple example of this would be to +convert an Option to a List. + +```scala +import scalaz.~> +object optionToList extends (Option ~> List) { + // pretending toList is not present + def apply[A](given: Option[A]): List[A] = given match { + case Some(a) => a :: Nil + case None => Nil + } +} +``` + +So now you have seen an example implementation of a Natural Transformation from one Higher Kinded Type (Option) to +another (List). We use the Natural Transformation in Free Monads to convert from our Instruction that is a +higher-kinded-type to a higher-kinded-type that must have an implementation of the Monad typeclass. So the definition of +an interpreter looks like this: + +```scala + /** + * Relevant snippet taken from ScalaZ + * + * Catamorphism for `Free`. + * Runs to completion, mapping the suspension with the given transformation at each step and + * accumulating into the monad `M`. + */ + final def foldMap[M[_]](f: S ~> M)(implicit M: Monad[M]): M[A] = + step match { + case Return(a) => M.pure(a) + case Suspend(s) => f(s) + // This is stack safe because `step` ensures right-associativity of Gosub + case a@Gosub(_, _) => M.bind(a.a foldMap f)(c => a.f(c) foldMap f) + } +``` + +As you can see the result higher-kinded-type `M` in this example, must have a Monad typeclass implementation. + +##### Why is all this needed? +Coyoneda says that we can turn any Abstract Data Type into a Functor (for free) given you provide a conversion later. +The Free Functor doesn't know how to `map`. So whenever you `map` over a Free Functor, it will accumulate your `map`s so +that it will be able to run it later. + +Likewise, a similar situation exists to get a Monad for free. You can provide any Functor and get a Monad (for free) +given you provide a conversion to a real Monad later. Similar to the Free Functor, the Free Monad doesn't know how to +`join` or `flatMap`. So whenever you `flatMap`, it will accumulate your `flatMap`s so that it will be run later when the +conversion is given. You end up with a recursive data structure when you perform those flatMaps (`F[F[F[F[F[F]]]]]`) and +when you provide a conversion to a real monad, you are able to shed the layers (hence the `foldMap`). One more thing to +note is that a Monad's `map` is a derived combinator meaning you get `map` for free if you implement `flatMap` and `pure` +so you meet the requirement for having a conversion both for the Free Monad and Free Functor if you provide a conversion +to a real Monad. This pays the cost of getting a Free Functor and a Free Monad. + +######So how do I do the conversion from the Free Monad to a real Monad? +A Natural Transformation and the imposition that the result data type of the Natural Transformation has a Monad +implementation. + +Note that all of this happens behind the scenes so we get to do very minimal work to use this tool. +Our `AppInstruction` data-type isn't a Functor so Coyoneda is used to get a Free Functor and then a Free Monad is used +to get a Monad from a Functor (buy-now, pay-later). + +When we write the interpreter (Natural Transformation + Monad imposition), we pay the cost. Here's what an example +interpreter looks like along with the description of the computation. This works if you bring in ScalaZ: + +```scala +import scalaz.{Free, ~>} + +object Example extends App { + + case class Person(id: String) + + sealed trait AppInstruction[Result] + case class GetById(id: String) extends AppInstruction[Option[Person]] + case class Save(person: Person) extends AppInstruction[Unit] + case class Log(info: String) extends AppInstruction[Unit] + + // smart constructors to lift the instruction data-types into Free + def getById(id: String): Free[AppInstruction, Option[Person]] = Free.liftF(GetById(id)) + def save(person: Person): Free[AppInstruction, Unit] = Free.liftF(Save(person)) + def log(info: String): Free[AppInstruction, Unit] = Free.liftF(Log(info)) + + // an example sequence of instructions (haven't executed anything yet) + val result: Free[AppInstruction, Option[Person]] = for { + _ <- log("about to save a Person") + _ <- save(Person("1")) + _ <- log("about to get a Person") + optPerson <- getById("1") + } yield optPerson + + type Id[A] = A + val exampleInterpreter = new (AppInstruction ~> Id) { + override def apply[A](fa: AppInstruction[A]): Id[A] = fa match { + case GetById(_) => + Some(Person("1")) + + case Save(_) => + () + + case Log(info) => + println(info) + () + } + } + + // run the instructions using the example interpreter + println(result.foldMap(exampleInterpreter)) +} +``` + +This is what the use of a Free Monad looks like. Notice the instructions are pure and we only side-effect when we run +the instructions against the `exampleInterpreter` + +In the application, a big interpreter is used which is composed of little interpreters which know how to handle a +certain concern. The big interpreter dispatches specific instructions to the specific little interpreter + +The `Http` trait is what I would consider the 'end-of-the-program' if you consider each web request-response is a +program. The `Http` trait demands for the general 'big' interpreter so it is unaware of the little interpreters so it +can run the free instructions and give meaning to them which forms the response to return to the user. + +The main concepts revolve around the Free Monad which is also known as the Interpreter pattern. I do use Circe to do the +JSON encoding and decoding so you will see some imports scattered around. I have placed the instructions in the `domain` +package and the interpreters in the `interpreter` package. + +## TO-DOs +Implement some 'real' interpreters as opposed to using toy interpreters +- Interpreter for SLF4J +- Interpreter for DynamoDB + + +## Contributing +Feel free to send in PRs or tell me if I've missed something (which I most certainly have). I do admit that I have +glossed over some concepts. \ No newline at end of file diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..581e875 --- /dev/null +++ b/build.sbt @@ -0,0 +1,20 @@ +name := "free-monad-experiment" + +version := "1.0" + +scalaVersion := "2.11.8" + +libraryDependencies ++= { + val http4s = "org.http4s" + val https4sV = "0.14.11a" + Seq( + http4s %% "http4s-core" % https4sV, + http4s %% "http4s-dsl" % https4sV, + http4s %% "http4s-blaze-server" % https4sV, + http4s %% "http4s-circe" % https4sV, + "io.circe" %% "circe-generic" % "0.4.1", + "org.scalaz" %% "scalaz-core" % "7.2.7", + "org.slf4j" % "slf4j-api" % "1.7.21", + "ch.qos.logback" % "logback-classic" % "1.1.7" + ) +} \ No newline at end of file diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..e0cbc71 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 0.13.13 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..14a6ca1 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +logLevel := Level.Warn \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..3fadba4 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %level %logger{15} - %message%n%xException{10} + + + + + + + \ No newline at end of file diff --git a/src/main/scala/com/experiments/calvin/Application.scala b/src/main/scala/com/experiments/calvin/Application.scala new file mode 100644 index 0000000..a8aa83b --- /dev/null +++ b/src/main/scala/com/experiments/calvin/Application.scala @@ -0,0 +1,23 @@ +package com.experiments.calvin + +import com.experiments.calvin.domain.AppInstruction +import com.experiments.calvin.interpreters.{AppInstructionInterpreter, InMemoryDbInterpreter, PrintlnLogInterpreter} +import org.http4s.server.{Server, ServerApp} +import org.http4s.server.blaze._ + +import scalaz.concurrent.Task +import scalaz.~> + +object Application extends ServerApp with Http { + + override def server(args: List[String]): Task[Server] = { + BlazeBuilder + .bindHttp(9000, "localhost") + .mountService(helloWorldService, "/") + .mountService(personService, "/") + .start + } + + override val interpreter: ~>[AppInstruction, Task] = + new AppInstructionInterpreter(new PrintlnLogInterpreter, new InMemoryDbInterpreter).interpreter +} diff --git a/src/main/scala/com/experiments/calvin/Http.scala b/src/main/scala/com/experiments/calvin/Http.scala new file mode 100644 index 0000000..3bc76e8 --- /dev/null +++ b/src/main/scala/com/experiments/calvin/Http.scala @@ -0,0 +1,56 @@ +package com.experiments.calvin + +import org.http4s._ +import org.http4s.dsl._ +import org.http4s.circe._ +import io.circe._ +import io.circe.syntax._ +import io.circe.generic.auto._ +import domain._ + +import scalaz.concurrent.Task +import scalaz.~> + +trait Http { + val interpreter: AppInstruction ~> Task + + val helloWorldService = HttpService { + case GET -> Root / "hello" => + Ok("Hello there") + + case GET -> Root / "async-hello" => + Ok(Task("Hello there")) + } + + val personService = HttpService { + case body@POST -> Root / "persons" => + body.as(jsonOf[Person]) + .flatMap(person => { + val savePerson = for { + _ <- log(s"Saving Person: $person") + _ <- save(person) + } yield () + savePerson.foldMap(interpreter) + }) + .flatMap(_ => Ok()) + + case GET -> Root / "persons" / personId => + val optPerson = for { + _ <- log(s"Obtaining person with id: $personId") + optPerson <- getById(personId) + } yield optPerson + + val tskOptPerson: Task[Option[Person]] = optPerson.foldMap(interpreter) + // Note that we flatMap on the Task, there is an implicit conversion + // for Http Status Codes which are Tasks so this is the appropriate use + // of flatMap since the mapper function is nested in that it produced a Task + // using an implicit conversion + tskOptPerson + .map(optPerson => optPerson.map(_.asJson)) + .flatMap(optJson => + optJson + .map(json => Ok(json)) + .getOrElse(NotFound()) + ) + } +} diff --git a/src/main/scala/com/experiments/calvin/domain/package.scala b/src/main/scala/com/experiments/calvin/domain/package.scala new file mode 100644 index 0000000..541eef4 --- /dev/null +++ b/src/main/scala/com/experiments/calvin/domain/package.scala @@ -0,0 +1,28 @@ +package com.experiments.calvin + +import scalaz.Free + + +package object domain { + + case class Person(username: String, firstName: String, lastName: String) + + // general application instruction + sealed trait AppInstruction[Result] { + def lift: Free[AppInstruction, Result] = Free.liftF(this) + } + + // more specific types of instructions + sealed trait LogInstruction[Result] + sealed trait DatabaseInstruction[Result] + + // AST + case class GetById(id: String) extends AppInstruction[Option[Person]] with DatabaseInstruction[Option[Person]] + case class Save(person: Person) extends AppInstruction[Unit] with DatabaseInstruction[Unit] + case class Log(info: String) extends AppInstruction[Unit] with LogInstruction[Unit] + + // Smart constructors + def getById(id: String): Free[AppInstruction, Option[Person]] = GetById(id).lift + def save(person: Person): Free[AppInstruction, Unit] = Save(person).lift + def log(info: String): Free[AppInstruction, Unit] = Log(info).lift +} diff --git a/src/main/scala/com/experiments/calvin/interpreters/package.scala b/src/main/scala/com/experiments/calvin/interpreters/package.scala new file mode 100644 index 0000000..8122f1c --- /dev/null +++ b/src/main/scala/com/experiments/calvin/interpreters/package.scala @@ -0,0 +1,47 @@ +package com.experiments.calvin + +import com.experiments.calvin.domain._ + +import scalaz.concurrent.Task +import scalaz.~> + +package object interpreters { + trait LogInterpreter { + def run[A](logInstruction: LogInstruction[A]): Task[A] + } + + class PrintlnLogInterpreter extends LogInterpreter { + def run[A](logInstruction: LogInstruction[A]): Task[A] = logInstruction match { + case Log(info) => Task(println(info)) + } + } + + trait DatabaseInterpreter { + def run[A](dbInstruction: DatabaseInstruction[A]): Task[A] + } + + class InMemoryDbInterpreter extends DatabaseInterpreter { + var map = Map.empty[String, Person] + + override def run[A](dbInstruction: DatabaseInstruction[A]): Task[A] = dbInstruction match { + case GetById(id) => + Task(map.get(id)) + + case Save(person) => + map = map + (person.username -> person) + Task(()) + } + } + + class AppInstructionInterpreter(logInterpreter: LogInterpreter, dbInterpreter: DatabaseInterpreter) { + val interpreter: AppInstruction ~> Task = new (AppInstruction ~> Task) { + override def apply[A](fa: AppInstruction[A]): Task[A] = fa match { + case logInstruction: LogInstruction[A] => + logInterpreter.run(logInstruction) + + case dbInstruction: DatabaseInstruction[A] => + dbInterpreter.run(dbInstruction) + } + } + } +}