From fadfbc8ea895f5bf7b72f61c3e29611cf9784b5b Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sat, 30 Jan 2016 13:51:11 -0500 Subject: [PATCH] Update free monad docs This resolves #804. I also updated the "pure" compiler to use `State`. It previously had said that a pure compiler couldn't be written and used with `foldMap`, but that's not really true if you use something like `State`. I think this is probably a cleaner and more useful solution. --- docs/src/main/tut/freemonad.md | 150 ++++++++++----------------------- 1 file changed, 45 insertions(+), 105 deletions(-) diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 94a3c9e9ff..329bc2ec50 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -77,30 +77,21 @@ recursive values. We need to create an ADT to represent our key-value operations: ```tut:silent -sealed trait KVStoreA[+Next] -case class Put[T, Next](key: String, value: T, next: Next) extends KVStoreA[Next] -case class Get[T, Next](key: String, onResult: T => Next) extends KVStoreA[Next] -case class Delete[Next](key: String, next: Next) extends KVStoreA[Next] +sealed trait KVStoreA[A] +case class Put[T](key: String, value: T) extends KVStoreA[Unit] +case class Get[T](key: String) extends KVStoreA[Option[T]] +case class Delete(key: String) extends KVStoreA[Unit] ``` -The `next` field in each of the types provides a way to link an -operation with successive values. The `Next` type parameter can be -anything at all, including `Unit`. It can be thought of as a carrier, -a way to link a single operation with successive operations. - -As we will see, the `next` field is also necessary to allow us to -provide a `Functor` instance for `KVStoreA[_]`. - ### Free your ADT There are six basic steps to "freeing" the ADT: 1. Create a type based on `Free[_]` and `KVStoreA[_]`. -2. Prove `KVStoreA[_]` has a functor. -3. Create smart constructors for `KVStore[_]` using `liftF`. -4. Build a program out of key-value DSL operations. -5. Build a compiler for programs of DSL operations. -6. Execute our compiled program. +2. Create smart constructors for `KVStore[_]` using `liftF`. +3. Build a program out of key-value DSL operations. +4. Build a compiler for programs of DSL operations. +5. Execute our compiled program. #### 1. Create a `Free` type based on your ADT @@ -110,33 +101,7 @@ import cats.free.Free type KVStore[A] = Free[KVStoreA, A] ``` -#### 2. Prove `KVStoreA[_]` has a `Functor`. - -One important thing to remark is that `Free[F[_], A]` automatically -gives us a monad for `F`, if `F` has a functor (i.e. if we can find or -construct a `Functor[F]` instance). It is described as a *free monad* -since we get this monad "for free." - -Therefore, we need to prove `KVStoreA[_]` has a functor. - -```tut:silent -import cats.Functor - -implicit val functor: Functor[KVStoreA] = - new Functor[KVStoreA] { - def map[A, B](kvs: KVStoreA[A])(f: A => B): KVStoreA[B] = - kvs match { - case Put(key, value, next) => - Put(key, value, f(next)) - case g: Get[t, A] => // help scalac with parametric type - Get[t, B](g.key, g.onResult andThen f) - case Delete(key, next) => - Delete(key, f(next)) - } - } -``` - -#### 3. Create smart constructors using `liftF` +#### 2. Create smart constructors using `liftF` These methods will make working with our DSL a lot nicer, and will lift `KVStoreA[_]` values into our `KVStore[_]` monad (note the @@ -147,31 +112,31 @@ import cats.free.Free.liftF // Put returns nothing (i.e. Unit). def put[T](key: String, value: T): KVStore[Unit] = - liftF(Put(key, value, ())) + liftF[KVStoreA, Unit](Put[T](key, value)) // Get returns a T value. -def get[T](key: String): KVStore[T] = - liftF(Get[T, T](key, identity)) +def get[T](key: String): KVStore[Option[T]] = + liftF[KVStoreA, Option[T]](Get[T](key)) // Delete returns nothing (i.e. Unit). def delete(key: String): KVStore[Unit] = - liftF(Delete(key, ())) + liftF(Delete(key)) // Update composes get and set, and returns nothing. def update[T](key: String, f: T => T): KVStore[Unit] = for { - v <- get[T](key) - _ <- put[T](key, f(v)) + vMaybe <- get[T](key) + _ <- vMaybe.map(v => put[T](key, f(v))).getOrElse(Free.pure(())) } yield () ``` -#### 4. Build a program +#### 3. Build a program Now that we can construct `KVStore[_]` values we can use our DSL to write "programs" using a *for-comprehension*: ```tut:silent -def program: KVStore[Int] = +def program: KVStore[Option[Int]] = for { _ <- put("wild-cats", 2) _ <- update[Int]("wild-cats", (_ + 12)) @@ -182,30 +147,9 @@ def program: KVStore[Int] = ``` This looks like a monadic flow. However, it just builds a recursive -data structure representing the sequence of operations. Here is a -similar program represented explicitly: - -```tut:silent -val programA = - Put("wild-cats", 2, - Get("wild-cats", { (n0: Int) => - Put("wild-cats", n0 + 12, - Put("tame-cats", 5, - Get("wild-cats", { (n1: Int) => - Delete("tame-cats", n1) - }) - ) - ) - }) - ) -``` - -One problem with `programA` you may have noticed is that the -constructor and function calls are all nested. If we had to sequence -ten thousand operations, we might run out of stack space and trigger a -`StackOverflowException`. +data structure representing the sequence of operations. -#### 5. Write a compiler for your program +#### 4. Write a compiler for your program As you may have understood now, `Free[_]` is used to create an embedded DSL. By itself, this DSL only represents a sequence of operations @@ -236,17 +180,17 @@ def impureCompiler = new (KVStoreA ~> Id) { def apply[A](fa: KVStoreA[A]): Id[A] = fa match { - case Put(key, value, next) => + case Put(key, value) => println(s"put($key, $value)") kvs(key) = value - next - case g: Get[t, A] => - println(s"get(${g.key})") - g.onResult(kvs(g.key).asInstanceOf[t]) - case Delete(key, next) => + () + case Get(key) => + println(s"get($key)") + kvs.get(key).map(_.asInstanceOf[A]) + case Delete(key) => println(s"delete($key)") kvs.remove(key) - next + () } } ``` @@ -271,7 +215,7 @@ behavior, such as: - a pseudo-random monad to support non-determinism - and so on... -#### 6. Run your program +#### 5. Run your program The final step is naturally running your program after compiling it. @@ -293,7 +237,7 @@ recursive structure by: This operation is called `Free.foldMap`: ```scala -final def foldMap[M[_]](f: S ~> M)(implicit S: Functor[S], M: Monad[M]): M[A] = ... +final def foldMap[M[_]](f: S ~> M)(M: Monad[M]): M[A] = ... ``` `M` must be a `Monad` to be flattenable (the famous monoid aspect @@ -302,7 +246,7 @@ under `Monad`). As `Id` is a `Monad`, we can use `foldMap`. To run your `Free` with previous `impureCompiler`: ```tut -val result: Int = program.foldMap(impureCompiler) +val result: Option[Int] = program.foldMap(impureCompiler) ``` An important aspect of `foldMap` is its **stack-safety**. It evaluates @@ -317,27 +261,23 @@ data-intensive tasks, as well as infinite processes such as streams. #### 7. Use a pure compiler (optional) The previous examples used a effectful natural transformation. This -works, but you might prefer folding your `Free` in a "purer" way. - -Using an immutable `Map`, it's impossible to write a natural -transformation using `foldMap` because you would need to know the -previous state of the `Map` and you don't have it. For this, you need -to use the lower level `fold` function and fold the `Free[_]` by -yourself: +works, but you might prefer folding your `Free` in a "purer" way. The +[State](state.html) data structure can be used to keep track of the program +state in an immutable map, avoiding mutation altogether. ```tut:silent -// Pure computation -def compilePure[A](program: KVStore[A], kvs: Map[String, A]): Map[String, A] = - program.fold( - _ => kvs, - { - case Put(key, value, next) => // help scalac past type erasure - compilePure[A](next, kvs + (key -> value.asInstanceOf[A])) - case g: Get[a, f] => // a bit more help for scalac - compilePure(g.onResult(kvs(g.key).asInstanceOf[a]), kvs) - case Delete(key, next) => - compilePure(next, kvs - key) - }) +import cats.state.State + +type KVStoreState[A] = State[Map[String, Any], A] +val pureCompiler: KVStoreA ~> KVStoreState = new (KVStoreA ~> KVStoreState) { + def apply[A](fa: KVStoreA[A]): KVStoreState[A] = + fa match { + case Put(key, value) => State.modify(_.updated(key, value)) + case Get(key) => + State.pure(kvs.get(key).map(_.asInstanceOf[A])) + case Delete(key) => State.modify(_ - key) + } +} ``` (You can see that we are again running into some places where Scala's @@ -345,7 +285,7 @@ support for pattern matching is limited by the JVM's type erasure, but it's not too hard to get around.) ```tut -val result: Map[String, Int] = compilePure(program, Map.empty) +val result: (Map[String, Any], Option[Int]) = program.foldMap(pureCompiler).run(Map.empty).value ``` ## Composing Free monads ADTs.