St.Antario
St.Antario

Reputation: 27455

Understanding Free monad in scalaz

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

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

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

Related Questions