Writing DSLs using Scala. Part 1 — Underlying Concepts

I've been using Scala in (not particularly torrid) anger for around two years now, and the Scala feature I've loved most during this time has been its ability to define concise Domain Specific Languages (DSLs). What got me into Scala in the first place was the fact that it was incredibly easy to write tests in Scala that looked for all intents and purposes like a Gherkin spec.

So in this series of blogposts, I am going to attempt to illustrate how you can write a simple matcher DSL. In the first part of the series, I'd like to illustrate some of the Scala features that allow us to build these powerful DSLs.

 Running the examples

All of the examples below may be found in Scala worksheets in the info.siddhuw.worksheets package of the Github project that I've put together as an accompaniment for this series of blogposts. All of the files that illustrate the concepts below have been tagged "blogpost-part-1".

You can clone the code (which is a Maven project) using this command:

git clone https://github.com/siddhuwarrier/scala-dsl-tutorial.git

The easiest way to run the worksheets is to import the Maven project into your IDE of choice, and then run the Scala worksheets. I haven't added a Maven target to run the worksheets.

The Optional Dot membership operator

In Scala, you can replace the familiar dot membership operator with a space.
Let us assume you have a class Foo with a method bar (yes, I know, how imaginative of me!)

class Foo {
    def bar = "This is the bar method"
}

You can call the method bar on an object of type Foo in one of these ways:

val foo = new Foo()
foo.bar
foo bar

You can try out this example by running the OptionalDotMembership.sc Scala worksheet. You may also notice that foo.bar() does not compile. This is because I did not specify an empty parameter list, and Scala thinks you're trying to retrieve a character in the string by index.

Functions are first-class values

Scala functions can be passed around (as arguments to functions, for example) just like any other objects. To illustrate, take a look at this (extremely contrived) example.

case class Person(name: String, age: Int) {
  def manipulate(func: Person => Person): Person = {
    func(this)
  }
}

The manipulate method takes a function which takes one argument of type Person and returns a Person. If we were now to write two methods like so:

def doubleAge(person: Person): Person = {
  Person(person.name, person.age * 2)
}

def addSillySuffixToPersonName(suffix: String, person: Person): Person = {
  Person(person.name + suffix, person.age)
}

(Yes, you may notice the second method does not match the signature of the function expected by the manipulate method, but hold that thought).

To double the age of a person, you can make a function call like so:

val p = Person("Kim", 31)
p.manipulate(doubleAge)

It gets a little trickier with the addSillySuffixToPersonName method. We need to create a function f that only takes a single argument of type Person. So, as addSillySuffixToPersonName takes two arguments, we create a partially applied function.

val func = addSillySuffixToPersonName(suffix = " is ill", _)

func now matches the signature of the function that we need to pass into the manipulate method.

p.manipulate(func)

You can try this out by running the FirstClassFunctions.sc Scala worksheet.

Implicit methods (and pimping)

Implicit methods are a mechanism whereby you can extend the functionality of a class with new methods without using inheritance. In conjunction with 'pimping' (for want of a less crude word), it can be quite powerful.

To illustrate, let us say we wish to add a method foo to an object of any type so that we can perform operations like these:

"my string".foo
1.foo

To do so, we first define a 'pimped' class as shown below:

case class PimpedAny[T](item: T) {
    def foo = "Item was pimped"
}

We then need to define an implicit method that will transformed an object of type T into a PimpedAny object (whenever the method is in scope).

implicit def to[T](item: T): PimpedAny[T] = PimpedAny(item)

When the to method above is in scope, the methods of both type T and PimpedAny are available on objects of type T.

You can try out this example by running the Pimping.sc Scala worksheet.

Implicit Classes

This is a Scala 2.10+ feature described in detail here. In essence, implicit classes are a way in which to provide extension methods to another type (rather like implicit methods), but in this specific case (from a DSL perspective), allows you to define infix operators for types.

For example, let's try to add a new operator +: to objects of type Int, which creates a list out of its operands. To wit,

1 +: 2 ----> List(1,2)
1 +: 2 +: 3 ---> List(1, 2, 3) 

To do this, we need to define an implicit class ExtendedInt which takes an integer as an argument (implicit classes have to take exactly one argument)

implicit class ExtendedInt(i: Int) {
  def +:(other: Int) = List(other, i)
}

Et voila! You can try this example out by running the ImplicitClasses.sc Scala worksheet.