Rolling Your Own DSL in Scala
Getting started with your own domain-specific languages is easy with Scala. This guide walks you through the basics of creating a DSL for your own use.
Join the DZone community and get the full member experience.
Join For FreeCreating your own DSL in Scala is very easy. You don't need language proficiency at an expert level for this. This post shows you how to get started. It is not our scope to teach basic concepts of Scala, like traits and objects, but even if you are not familiar with the language, it should be possible to follow the logic here.
I have been dabbling in Scala for some time. At my place of work, though, it does not seem feasible to use Scala for code in production, so I decided that I could get away with using it in unit testing at least. Since I am of the school of total white-box-testing, I do not hesitate to set and access private members of classes whenever it saves a clean design from being disrupted. I know doing so is a bone of contention, but this is not what this post is about – it’s what the DSL is about.
In Java, as in Scala, accessing private class members by reflection is quite tedious and repetitive, so I decided to write a utility for that. And, using Scala, I had something like this in mind:
val bar = new Bar
set field “foo” of bar to “baz”
val qux = value of method “someInternalMethod” of bar
Looks nice, right? This would make use of the fact that Scala allows one-argument methods without dot operator and parentheses. So, how to go about it? Regarding Scala, I would characterize myself as an advanced beginner, so my toolset does not include any arcane stuff that only gurus understand. But wait, this can't be so difficult. Let’s take it piece by piece.
set field “foo” of bar to “baz”
Ok, to begin with, I need something called set
. This should probably be an instance of some class. How do I get it into my unit test class? Well, a trait should do fine.
trait Decapsulation {
val set = new Set()
}
Then I can use it like this:
@RunWith(classOf[JUnitRunner])
class BarTest extends FlatSpec with Decapsulation {
"A bar" should "foo" in {
val bar = new Bar
set field "foo" of bar to "baz"
// do more stuff and assert something now
// ...
}
}
For those not familiar with Scala, this exemplifies the structure of a FlatSpec
-based, behavior-driven test.
It looks like set
has a method called field
, and this method takes a string parameter (“foo” in the example). Since there seems to be only one instance of set
necessary, we can make it a Scala object instead of a class.
object Set {
def field(name: String): ??? = ???
}
Then the trait can look like this:
trait Decapsulation {
val set = Set
}
The field
method must return something that has a method called of
. And this method takes any type of object as a parameter (the object under test bar
in our case). Right, let’s create a class Of
(call it whatever you like, it does not matter for the DSL. What matters is the name of the method):
class Of(val name: String) {
def of(o: AnyRef): ??? = ???
}
We will see in a minute that we need different instances of Of
, so Of
needs to be a class, not a Scala object. The field
method returns an instance of an Of
. On this instance we call the method (lowercase) of
. The field
method looks like this now:
def field(name: String): Of = new Of(name)
Remember – name
is the name of the field we are going to manipulate and we hand that over to the new instance of Of
. Now, the of
method needs to return something we can use to call to
on. Well, I guess, you get the hang of it by now ...
class To(name: String, obj: AnyRef) {
def to(value: Any):??? = ???
}
This makes the of
method look like this:
def of(obj: AnyRef): To = new To(name, obj)
We pass our collected arguments (field name and the object containing the field) to a new To
instance. The (lowercase) to
method can get the new value for our field as the single parameter. And since we now have all we need, we can do the tedious reflection call and actually set the field:
def to(value: Any): Unit = {
val f = obj.getClass.getDeclaredField(name)
f.setAccessible(true)
f.set(obj, value)
}
That’s it, really – this is all there is to get started on a DSL in Scala. In hindsight, it looks almost trivially easy. The example is highly simplified – the field might belong to a base class or be final or static or both or whatever – but all this can be handled in gory detail in the to
method.
By the way, the DSL works in Java, too:
@Test
public void test() {
Bar bar = new Bar();
Set.field("foo").of(bar).to("baz");
// do more stuff and assert something now
// ...
}
Just import the Set
class — field
is a static method of the class.
In conclusion, there are these principles at work:
- Start with the instance of a class or a Scala object that represents the first instruction.
- Use one class for each instruction so the user is guided through the statement and autocompletion in IDEs works correctly. The class contains a method (or several methods, if there are branches), that represents the current instruction.
- The methods take arguments if necessary. If each method takes maximally one argument, there is no need for parentheses and the DSL looks rather like a natural language.
Pass through all data you collect as constructor arguments to the next class on the way to the final instruction in the DSL statement and process them there.
The complete working code is right here. Paste it into a Scala file and test it.
package getting.started.on.a.dsl.in.scala
trait Decapsulation { val set = Set }
object Set { def field(name: String): Of = new Of(name) }
class Of(val name: String) { def of(obj: AnyRef): To = new To(name, obj) }
class To(name: String, obj: AnyRef) {
def to(value: Any): Unit = {
val f = obj.getClass.getDeclaredField(name)
f.setAccessible(true)
f.set(obj, value)
}
}
I leave it to you to figure out how to implement:
val qux = value of method “someInternalMethod” of bar
It should not be difficult after the field example. One hint though — you'll need to call the container class for the new of
method something other than Of
, or put it in another package than the field's Of
class. And probably generics need to come into play to make the instruction return objects of the right type instead of Nothing
.
Opinions expressed by DZone contributors are their own.
Comments