Scala DSL without extra syntax
I asked myself this question a couple of times and came up with a solution for that feels very dirty. Maybe you can give me any advice since I think this is a basic problem for every DSL written in Scala.
I want to have a hierarchical structure of nested objects without adding any extra syntax. Specs is a good example for this:
MySpec extends Specification {
"system" should {
"example0" in { ... }
"example1" in { ... }
"example2" in { ... }
}
"system" can {
"example0" in { ... }
}
}
For instance I do not have to write "example0" in { ... } :: "example1" in { ... } :: "example2" in { ... } :: Nil
.
This is exactly the same behaviour I would like to have. I think this is achieved by an implicit definition in the Specification class in Specs like (please do not be offended if you are the Specs author and I missunderstood something :))
implicit def sus2spec(sus: Sus): Specification = {
suslist += sus
this
}
My main problem arises now when I want to nest such objects. Imagine I have this grammar:
root: statement*;
statement:
IDENT '{' statement* '}'
| declaration*
;
declaration: IDENT ':=' INT+;
I would like to translate this into a开发者_Python百科 DSL that looks like this:
MyRoot extends Root {
"statement0" is {
"nested_statement0" is {
"nested_nested_statement0" is {
"declaration0" := 0
}
"declaration1" := 1
"declaration2" := 2
}
"declaration3" := 3
}
"statement1" is {
"declaration4" := 4
}
}
The problem that arises here is for me that the implicit solution does not work. The implicit definition would be executed in the scope of the root object which means I would add all objects to the root and the hierarchy is lost.
Then I thought I can use something like a Stack[Statement]. I could push an object to it for every call to is
but that feels very dirty.
To put the question in one sentence: How do I create a recursive DSL wich respect to its hierarchy without adding any extra syntax and is there a solution to do this with immutable objects only?
I've seen a nice trick in XScalaWT to achieve the nesting in DSL. I didn't check if specs uses the same, or something different.
I think the following example shows the main idea. The heart of it is the setups function: it accepts some functions (more precisely closures, if I'm not mistaken) that needs only a Nestable and will call them on the current one.
printName happens to be such a method, just like addChild, with parameters filled for the first list of params.
For me understanding this was the revealing part. After that you can relatively simply add many other fancy features (like implicit magic, dsl methods based on structural typing, etc.).
Of course you can have any "context like" class instead of Nestable, especially if you go for pure immutable stuff. If parents need references to children you can collect the children during the setups() and create the parent only at the end.
In this case you would probably have something like
private def setupChildren[A, B](a : A, setups:(A => B)*) : Seq[B] = {
for (setup <- setups) yield setup(a)
}
You would pass in the "context", and create the parent using the returned children.
BTW I think this setup thing was needed in XScalaWT because it's for SWT where child objects need a reference to their parent control. If you don't need it (or anything from the current "context") then everything becomes a bit easier.
Using companion objects with proper apply methods should mostly solve the problem. Most likely they should also accept other functions, (having the same number of params, or a tuple if you need more).
One disadvantage of this trick is that you have to have a separate dsl method (even if a simple one) for each method that you want to call on your classes. Alternatively you can use lines like
x => x.printName
which will do the job, but not so nice (especially if you have to do it often).
object NestedDsl {
object Nestable {
def apply(name: String, setups:(Nestable => Unit)*): Nestable = {
val n = new Nestable(None, name)
setup(n, setups: _*)
n
}
}
class Nestable(parent: Option[Nestable], name: String) {
def printName() { println(name) }
}
// DSL part
def addChild(name: String, setups:(Nestable => Unit)*)(parent: Nestable) = {
val n = new Nestable(Some(parent), name)
setup(n, setups: _*)
n
}
def printName(n: Nestable) = n.printName
private def setup[T](t : T, setups:(T => Unit)*) : T = {
setups.foreach(setup => setup(t))
t
}
def main(args: Array[String]) {
Nestable("root",
addChild(
"first",
addChild("second",
printName
)
)
)
}
}
I have had a look at specs and they do not do it any differnet. Basically all you need is a mutable stack. You can have a look at the result here: cssx-dsl
The code is quite simple. Basically I have a mutable builder and convert it to an immutable representation afterwards.
精彩评论