Friday, April 8, 2011

Internal DSLs in Scala

Quite some time ago I read the Programming in Scala and the Programming Scala book and decided to offer a student lab on a Scala-based implementation of a Reversi tournament framework. One part of this framework is a DSL for the configuration of a tournament. So I went on and investigated the Scala techniques that allow for the construction of internal DSLs. Although there is quite some information on the internet and in the books about Scala DSLs here is what I came up with to get my student started.

Example

So, here is an example DSL for tournaments and the list of the techniques that I encountered and consider essential for building internal DSLs in Scala. I don't claim - and I don't think - that this list is complete, though.

import scala.collection.mutable.ListBuffer

class Setup extends TournamentBuilder {
    "3:2" ==> {
      --> ("butcher" vs "hulk")
      --> ("chaos" vs "crusher")
      --> ("diabola" vs "electra")
    }
}

object Main {
  def main(args: Array[String]) {
    new Setup()
  }
}

class Tournament(val bestof: Int, val winners: Int) {
  def ==> (block: => TournamentBuilder) : Unit = 
  { 
    val config = block.list
    run(config)
  }

  def run(config: ListBuffer[(String, String)]) 
  {
    for (game <- config) {
      Console.print("running best of " + bestof + 
 " in " + game._1 + " vs. " + game._2 + "\n")
    }
    Console.print("The best " + winners + 
 " players now proceed to the next round\n")
  }
}

class MatchBuilder(p1: String) {
  def vs(p2: String) = (p1, p2)
}

class TournamentBuilder {
  implicit def strToTourn(mode: String): Tournament = 
  {
    val parts = mode.split(":")
    new Tournament(parts(0).toInt, parts(1).toInt)
  } 

  implicit def strToMatchB(p1: String): MatchBuilder = 
    new MatchBuilder(p1)

  var list = new ListBuffer[(String, String)]()

  def -->(config: (String, String)): TournamentBuilder = 
  {
    list += config
    this
  }
}

technique 1: method names

Scala allows to use almost any sequence of Unicode characters as method names. So, especially the ASCII artists can use their creativity and the others can check out the Unicode tables for arrows, mathematical operators, or any other proper symbols.

Be aware though, that the usage of the DSL becomes cumbersome, though, because Unicode symbols are usually not bound directly to some keys on the keyboard. (@Vim users: check out this howto). So, for my example I sticked to good old ASCII. And I wasn't very creative either. But I can assure you that ==> and --> have special semantics for tournament planners ;-)

technique 2: infix notation

In Scala, every method with a single parameter, i.e. any binary function, can be written in infix notation without parantheses. This increases the readability of the DSL because you can write "butcher" vs "hulk" instead of "butcher".vs("hulk") - which doesn't work yet because vs is not a method of String; see the next technique.

technique 3: implicit convertion

Scala's implicit feature is really powerfull and widely used behind the courtains. In the above example, the strToMatchB method is implicitly called whenever there is a String object but a MatchBuilder object is expected. So, "butcher" vs "hulk" becomes strToMatchB("butcher").vs("hulk"). The trigger for this implicit method call is the method vs which is only defined for MatchBuilder objects.

technique 4: code blocks

Code blocks, a.k.a. closures, are first-order citizens in the Scala world. The curly braces allow to denote literals and the type system provides types for them which are essentially simple function types. In the above example, the code block expects no parameters but results in a TournamentBuilder object. With this technique, you can provide grouping and scoping for your DSL.

technique 5: string processing

In the above example, "3:2" is supposed to be the tournament mode which reads as "every match is best of 3 and the best two players enter the next round". This is just an example to show that besides all DSL magic you can still use plain old strings and parse them. Either with simple string utility methods, with regular expressions or with parser combinators. Depending on the requirements of the DSL using small sub-grammars can greatly increase the readability of the language.

Execution

This program's output is:

running best of 3 in butcher vs. hulk
running best of 3 in chaos vs. crusher
running best of 3 in diabola vs. electra
The best 2 players now proceed to the next round

First, the string "3:2" is converted into a TournamentBuilder by strToTourn. Next, the method ==> is invoked which executes the passed code block. There, a chain of --> method calls is executed while each incarnation updates the member list and returns the same TournamentBuilder object. To this end, the --> method expects a tuple which is constructed by means of the MatchBuilder class as explained above. Finally, the Tournament simply takes the MatchBuilder's list as its configuration for the holding of the tournament.

Meta

I want to thank fire for being my sparring partner on programming languages in general and Scala in particular. Go and check out his blog for more good stuff about Scala.

No comments:

Post a Comment