Wednesday, August 17, 2011

Scala DSL technique - if-else constructs

Believe it or not, I've had a hard time coming up with this construct, so I figured it's worth publishing it, maybe save others the hassle.

You want to come up with a recursive if/else construct, like below (I used a dollar sign to denote the DSL if - I've used wif/welse in the past - use whatever you want):

  var branch = 1

  val expr =

    $if(branch == 1) {

      v("a") := 11

      v("b") := 12

    } $else $if(branch == 2) {

      v("a") := 21

      v("b") := 22

    } $else $if(branch == 3) {

      v("a") := 31

      v("b") := 32

    } $else {

      v("a") := 41

      v("b") := 42

    }


Note the recursive if-else-if-else-if-else. The idea is that the if/else token will modify its own else branch depending on the following else - either the final else {} or an else followed by an if:

  case class TIf(cond: () => Boolean, body: () => Unit, var elsebranch: Option[TElse] = None) extends Token("IF ") {

    val scope = TScope(body)

    def $else[T](body: => T) = { this.lastif.elsebranch = Some(TElse(() => body)); this }

    def $else(i: TIf) = {

      this.lastif.elsebranch = Some(TElseIf(i))

      this

    }

    def lastif = elsebranch.map(_.lastif).getOrElse(this)

    ...


Also note that the way scala works is that the if-else tree is built from the top, so each else() is actually invoked on the root IF, therefore it needs to recursively find the last unbound IF and bind itself there as the elsebranch.

Having said that, find the full code and use it as a template, here.

Enjoy!

P.S. One issue with the simplified version built here is that it doesn't stop you from simple syntax errors like having an if-else-else... how would you fix it so the compiler stops you rather than the runtime exception?

1 comments:

Paolo G. Giarrusso said...

Hi, I needed exactly this, thank you very much. I was trying to write something like that but wrote an elseif_ method - but that way, I couldn't call elseif_ but had to call .elseif_. After some painless Googling, I found your code, and integrated the missing idea into my code (with credit).

Regarding your question, here's my (simplified) version of your code. Note that it is purely functional, and that it prevents statically additional else_ branches, because else_ returns a different type; one could easily build a non-purely functional version using mutable collections, but I don't think it's worth it (not for my use-case anyway). Your code ends up implementing manually a linked list, in practice, and that's something you should avoid, especially in Scala where standard collections have so much to give.

So, here's the code (and apologies for the formatting, but the <pre> tag is forbidden here):

//Simplified code from my framework - similar to the Delite core:
trait Exp[+T] {
def interpret: T
}
implicit def toExp[T](t: T) = Const(t)
case class Const[T](t: T) extends Exp[T] {
def interpret = t
}

//The solution
case class IfThenElse[T](cond: Exp[Boolean], thenBody: Exp[T], elseBody: Exp[T])

case class Elseable[T](conds: Seq[Exp[Boolean]], bodies: Seq[Exp[T]]) {
def else_[U >: T](elseBody: Exp[U]): Exp[U] =
(conds, bodies).zipped.foldRight(elseBody) {
case ((cond, thenBody), curr) => IfThenElse(cond, thenBody, curr)
}
//This overload allows chaining if-else if. The idea comes from:
//http://blog.razie.com/2011/08/scala-dsl-technique-if-else-constructs.html
def else_[U >: T](branch: Elseable[U]) = Elseable(conds ++ branch.conds, bodies ++ branch.bodies)
}
def if_[T](cond: Exp[Boolean])(thenBody: Exp[T]) = Elseable(Seq(cond), Seq(thenBody))