Reputation: 27455
I'm experimenting with Free
monad in Scalaz
and trying to build simple interpreter to parse and evaluate expressions like:
dec(inc(dec(dec(10)))
where dec
means decrement, inc
means increment. Here is what I got:
trait Interpreter[A]
case class V[A](a: A) extends Interpreter[A]
object Inc {
private[this] final val pattern = Pattern.compile("^inc\\((.*)\\)$")
def unapply(arg: String): Option[String] = {
val m = pattern.matcher(arg)
if(m.find()){
Some(m.group(1))
} else None
}
}
object Dec {
private[this] final val pattern = Pattern.compile("^dec\\((.*)\\)$")
def unapply(arg: String): Option[String] = {
val m = pattern.matcher(arg)
if(m.find()){
Some(m.group(1))
} else None
}
}
object Val {
def unapply(arg: String): Option[Int] =
if(arg.matches("^[0-9]+$")) Some(Integer.valueOf(arg))
else None
}
Now this is all I need to build AST. It currently looks as follows:
def buildAst(expression: String): Free[Interpreter, Int] =
expression match {
case Inc(arg) => inc(buildAst(arg))
case Dec(arg) => dec(buildAst(arg))
case Val(arg) => value(arg)
}
private def inc(i: Free[Interpreter, Int]) = i.map(_ + 1)
private def dec(d: Free[Interpreter, Int]) = d.map(_ - 1)
private def value(v: Int): Free[Interpreter, Int] = Free.liftF(V(v))
Now when testing the application:
object Test extends App{
val expression = "inc(dec(inc(inc(inc(dec(10))))))"
val naturalTransform = new (Interpreter ~> Id) {
override def apply[A](fa: Interpreter[A]): Id[A] = fa match {
case V(a) => a
}
}
println(buildAst(expression).foldMap(naturalTransform)) //prints 12
}
And it works pretty much fine (I'm not sure about if it is in scalaz
style).
THE PROBLEM is the extractor objects Inc
, Dec
, Val
feels like boilerplate code. Is there a way to reduce such a code duplication.
This will definitely become a problem if the number of functions supported gets larger.
Upvotes: 1
Views: 177
Reputation: 27595
Free monads are creating some boilerplate and that is a fact. However if you are willing to stick to some conventions, you could rewrite interpreter with Freasy Monad:
@free trait Interpreter {
type InterpreterF[A] = Free[InterpreterADT, A]
sealed trait InterpreterADT[A]
def inc(arg: InterpreterF[Int]): InterpreterF[Int]
def dec(arg: InterpreterF[Int]): InterpreterF[Int]
def value(arg: Int): InterpreterF[Int]
}
and that would generate all of case classes and matching on them. The interpreter becomes just a trait to implement.
However, you already have some logic within unapply - so you would have to split the parsing and executing logic:
import Interpreter.ops._
val incP = """^inc\\((.*)\\)$""".r
val decP = """^dec\\((.*)\\)$""".r
val valP = """^val\\((.*)\\)$""".r
def buildAst(expression: String): InterpreterF[Int] = expression match {
case incP(arg) => inc(buildAst(arg))
case decP(arg) => dec(buildAst(arg))
case valP(arg) => value(arg.toInt)
}
Then you could implement an actual interpreter:
val impureInterpreter = new Interpreter.Interp[Id] {
def inc(arg: Int): Int = arg+1
def dec(arg: Int): Int = arg-1
def value(arg: Int): Int = arg
}
and run it:
impureInterpreter.run(buildAst(expression))
I admit that this is more of a pseudocode than tested working solution, but it should give a general idea. Another library that uses similar idea is Freestyle but they use their own free monads implementation instead of relying on a cats/scalaz.
So, I would say it is possible to remove some boilerplate as long as you have no issue with splitting parsing and interpretation. Of course not all can be removed - you have to declare possible operations on your Interpreter
algebra as well as you have to implement interpreter yourself.
Upvotes: 2