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