In Scalaz, the Free monad is like two cases (simplified and ignoring GoSub optimization):
sealed abstract class Free[S[_], A] case class Return[S[_], A](a: A) extends Free[S, A] case class Suspend[S[_], A](a: S[Free[S, A]]) extends Free[S, A]
First, see what Free.liftF does, for example. for
def get(key: String): Script[String] = liftF(Get(key, identity))
when executing get("key") we get
get("key") // definition of get liftF(Get("key",identity) // definition of liftF Suspend(Get("key",identity).map(Return) // definition of map for Get above Suspend(Get("key", identity andThen Return)) // identity andThen f == f Suspend(Get("key", Return))
After that, let's start with your questions:
- The sequence of linear operations (which is also a tree, of course) is represented by Suspend (Op (Suspend (Op (...... (Result (Op)) ..)) and, thus, is an AST representation? Is this correct assumption?
In fact, a program written in DSL using the free monad that emerges from a functor is a chain of “steps”, where each step is either Suspend containing one of your functor cases, or Return representing the end of the chain.
As a concrete example, a script looks something like this:
Suspend(Get("swiss-bank-account-id", id => Suspend(Get(id, v => Suspend(Put(id, v+1000000, _ => Suspend(Put("bermuda-airport","getaway car", _ => Suspend(Delete("tax-records", _ => Return(()) ))))))))))
As you can see, we essentially just “fill” the holes of our functor by continuing with the calculation, ending with a Return . In the DSL sample, we will always have a linear chain due to the fact that each case of the KVS functor has only one “hole” for filling, therefore there is no branching.
- How is a real AST represented in a free monad? I assume this happens when control structures are enabled? (for example, the left and right branches of a tree, depending on the state). Can someone illustrate an example when real ACTs come into play? Maybe an illustration of how "if" can be implemented in this example.
Let us expand our DSL with a branching construct:
case class Cond[Next](cond: Boolean, ifTrue: Free[KVS,Next], ifFalse: Free[KVS,Next]) extends KVS[Next] def cond[A](c: Boolean)(ifTrue: => Script[A])(ifFalse: => Script[A]): Script[A] = liftF(Cond(c,ifTrue,ifFalse))
after changing the cases of the interpreter, it can be used as follows:
val script2: Script[Unit] = for { id <- get("swiss-bank-account-id") _ <- cond(id == "123") { Free.point[KVS,Unit](()) } { for { _ <- modify(id, ("LOTS OF " + _)) _ <- put("bermuda-airport", "getaway car") _ <- delete("tax-records") } yield () } } yield ()
So, now you have a “real” AST, where I interpret your “real” as “has branch nodes” instead of a linear chain form, as it has been so far. Expected Output:
println(interpretPure( script2, Map("swiss-bank-account-id" -> "42", "42" -> "money", "tax-records" -> "acme corp"))) // Map(swiss-bank-account-id -> 42, 42 -> LOTS OF money, bermuda-airport -> getaway car) println(interpretPure( script2, Map("swiss-bank-account-id" -> "123", "tax-records" -> "acme corp"))) // Map(swiss-bank-account-id -> 123, tax-records -> acme corp)
- What is the general approach for including control structures in scripts (as stated in 5 in the source code?)
First of all, remember that you can, for example, use the standard, if inside for understanding:
val script3: Script[Unit] = for { id <- get("swiss-bank-account-id") _ <- if (id == "123") { Free.point[KVS,Unit](()) } else { for { _ <- modify(id, ("LOTS OF " + _)) _ <- put("bermuda-airport", "getaway car") _ <- delete("tax-records") } yield () } } yield ()
Secondly, remember that because Script[A] is just Free[KVS,A] , you have a monad at your disposal, so any "control structure" defined, for example, Scala for monads will also work for you:
val script4: Script[Unit] = modify("key",_+"!").untilM_ { get("key").map(_.length > 42) } println(interpretPure(script4, Map("key" -> "")))
Take a look at Monad.scala and MonadSyntax.scala , there are also whileM and iterateWhile .