Writing DSLs using Scala. Part II - A simple matcher DSL

This is the second of a two-part series of blogposts that explain how Scala can be used to create powerful Domain Specific Languages (DSLs). Before you read this, please read the first part, where I describe the Scala features that allow us to build these DSLs.

In this post, I shall illustrate how to build a Scala DSL by building a simple matcher DSL that will allow users of the DSL to perform assertions — in essence, a simplified version of a testing DSL like Scalatest or Specs 2.

Notes on the Testing Framework
As I plan to build a simple subset of a testing DSL as part of this post, I couldn't use a testing DSL to test it as I shall be implementing some of the same verbs. So I've decided to use plain ol' JUnit.
Running the example code
You can clone the accompanying code project from Github here:

git clone https://github.com/siddhuwarrier/scala-dsl-tutorial.git
git checkout tags/blogpost-part-2
    

DSL Feature: Equality assertion

The objective of this feature is to allow users of the DSL to perform assertions of equality as shown below:

case class Person(name: String)

Person("doggie") should equal (Person("doggie")) //should not fail
Person("doggie") should equal (Person("hawser")) //should throw an exception
Person("doggie") should equal ("sfsgfs") //should throw an exception
Person("doggie") should equal (null) //should throw an exception

The first thing we shall implement (after having written the tests, obviously) is the should infix operator, which we can implement as an implicit class within a trait that we shall call SimpleMatcherDsl. This operator should take two operands:
* An object of any type on the LHS, and * on the RHS, a matcher function that should take the left-hand operator and return a boolean (after presumably performing some sort of comparison between the left and right hand sides). If the matcher function returns false, the operation should fail with a TestFailedException.

implicit class ShouldOperator[T](leftOperand: T) {
    def should(matcherFunc: Any => Boolean): Unit = {
        if (!matcherFunc(leftOperand)) {
            throw new TestFailedException("Assertion failed")
        }
    }
}

The matcher function we wish to implement for this feature is, obviously, one called equal. In order to be passed in as the RHS of the should infix operator, he equal method should return a function with a Any=>Boolean signature (the fact that Scala functions are first-class values comes in handy here).

def equal(rightOperand: Any): Any => Boolean = {
    leftOperand:Any => {
        leftOperand equals rightOperand
    }
}

In the code snippet above, the equal method takes the right operand as an argument, while returning a function that takes the left operand as an argument. Upon invocation, the returned function will compare the rightOperand passed into the equal method against the leftOperand passed in as an argument to it.

And yes, that's all there is to it.

Note: All of the tests for this feature may be found in info.siddhuw.EqualityAssertionTest.scala file and the implementation itself is in info.siddhuw.SimpleMatcherDsl. The code at this point has been tagged blogpost-part-2-feature-1

This is the second of a two-part series of blogposts that explain how Scala can be used to create powerful Domain Specific Languages (DSLs). You can read the first part, where I describe the Scala features that allow us to build these DSLs, here.

In this post, I shall illustrate how to build a Scala DSL by building a simple matcher DSL that will allow users of the DSL to perform assertions — in essence, a simplified version of a testing DSL like Scalatest or Specs 2.

Notes on the Testing Framework
As I plan to build a simple subset of a testing DSL as part of this post, I couldn't use a testing DSL to test it as I shall be implementing some of the same verbs. So I've decided to use plain ol' JUnit.
Running the example code
You can clone the accompanying code project from Github here:

git clone https://github.com/siddhuwarrier/scala-dsl-tutorial.git
git checkout tags/blogpost-part-2
You can run the unit tests by typing {{mvn test}} into the command-line.

DSL Feature: Equality assertion

The objective of this feature is to allow users of the DSL to perform assertions of equality as shown below:

case class Person(name: String)

Person("doggie") should not equal (Person("doggie")) //should not fail
Person("doggie") should not equal (Person("hawser")) //should throw an assertion error
Person("doggie") should not equal ("sfsgfs") //should throw an assertion error
Person("doggie") should not equal (null) //should throw an assertion error

The first thing we shall implement (after having written the tests, obviously) is the should infix operator, which we can implement as an implicit class within a trait that we shall call SimpleMatcherDsl. This operator should take two operands:
* An object of any type on the LHS, and * on the RHS, a matcher function that should take the left-hand operator and return a boolean (after presumably performing some sort of comparison between the left and right hand sides). If the matcher function returns false, the operation should fail with a TestFailedException.

implicit class ShouldOperator[T](leftOperand: T) {
    def should(matcherFunc: Any => Boolean): Unit = {
        if (!matcherFunc(leftOperand)) {
            throw new TestFailedException("Assertion failed")
        }
    }
}

The matcher function we wish to implement for this feature is, obviously, one called equal. In order to be passed in as the RHS of the should infix operator, he equal method should return a function with a Any=>Boolean signature (the fact that Scala functions are first-class values comes in handy here).

def equal(rightOperand: Any): Any => Boolean = {
    leftOperand:Any => {
        leftOperand equals rightOperand
    }
}

In the code snippet above, the equal method takes the right operand as an argument, while returning a function that takes the left operand as an argument. Upon invocation, the returned function will compare the rightOperand passed into the equal method against the leftOperand passed in as an argument to it.

And yes, that's all there is to it.

Note: All of the tests for this feature may be found in info.siddhuw.EqualityAssertionTest.scala file and the implementation itself is in info.siddhuw.SimpleMatcherDsl. The code at this point has been tagged blogpost-part-2-feature-1

DSL Feature: Inequality

The objective of this feature is to allow users of the DSL to perform assertions of inequality as shown below:

case class Person(name: String)

Person("doggie") should equal (Person("hawser")) //should not fail
Person("doggie") should equal ("sfsgfs") //should not fail
Person("doggie") should equal (null) //should not fail
Person("doggie") should not equal (Person("doggie")) //should throw a exception

There are two ways in which we could approximate this functionality, one of which results in more parentheses than you'd like in a nice, clean DSL (but is far simpler).

The simple way

The easiest way to do this is to create a function called not, that takes a Any=>Boolean function f(x) as an argument, and returns another function g(x) which negates the result of f(x)

def not(func: Any => Boolean): Any => Boolean = {
    leftOperand => {
        !func(leftOperand)
    }
}

This would allow us to write an inequality assertion like so:

Person("doggie") should not(equal Person("howser"))

Pretty good, but as I'm sure you'll agree, not quite what we'd like to get to. Rather than a space between not and equal, we've ended up with a very unnatural parenthesis.

The nicer way

Note: The Scalatest code implements 'not equal' in a very similar fashion; my implementation, while far more limited, is drawn entirely from the way Scalatest implements it.

What we'd like to implement is a DSL specification that allows us to avoid the parentheses that plagued our simple approach.

Referring back to my previous blogpost, Scala allows the . membership operator to be replaced with a space. So, in our case, if not were an object, and equal were a method of the not object, we'd be able to replace
not(equal(x)) with not.equal(x) or better still, not equal x.

To this end, let's implement a class called NotKeyword:

sealed class NotKeyword
Digression: Sealed classes
Sealed classes and traits are classes or traits that cannot be inherited from outside of the file in which they have been declared.

Then, let's create an object called not of type NotKeyword

val not = new NotKeyword

Now, our first impulse would be to implement an equal method in the NotKeyword class (it was certainly my first impulse).

sealed class NotKeyword {
    def equals(rightOperand: Any): Any => Boolean = {
        leftOperand => {
            !(leftOperand equals rightOperand)
        }
    }
}

But if we were to do that and try to compile the statement 12 should equal 14, we'd get a compilation error. This is because the Scala operator precedence rules lead to

12 should not equal 14

being parsed as

12.should(not)[...]

because it evaluates the operation from left to right. Now, the should method does not accept a NotKeyword as an argument; it expects a function with an Any=>Boolean signature.

We could fix our statement above to compile and work correct by changing it to:

12 should (not equal 14)

But this is even less readable than the simpler approach we took in the previous section.

To fix this, let's look once again at how the Scala compiler evaluated our expression. It took the object not in as an argument to the should method (i.e., as the second operand to the should operator). So, we need to do two things:
* Create a should method signature that accepts a NotKeyword object as an argument. * Return an object of a different type (let's call it the NotKeywordResult) that has a method called equal which takes the right operand as an argument.

val notKeywordResult: NotKeywordResult = 12 should not
notKeywordResult.equal(14) //or better still...

notKeywordResult equal 14

Putting the above statements all together, we end up with a syntactically valid statement that looks like this:

12 should not equal 14

Yay!

The NotKeywordResult class may be implemented as follows:

sealed case class NotKeywordResult(leftOperand: Any) {
    def equal(rightOperand: Any): Unit = {
        if (leftOperand equals rightOperand) {
            throw new TestFailedException("Assertion failed")
        }
    }
}

And a new should method needs to be implemented inside the ShouldOperator implicit class:

def should(notKeyword: NotKeyword): NotKeywordResult = NotKeywordResult(leftOperand)

If you've stayed with me this far, you should now be the proud possessor of a pale Scalatest/Specs 2 imitation that serves no practical purpose whatsoever. However, the principles we applied here are analogous to those we'd need to apply in order to build less trivial DSLs.