Saturday, September 15, 2007

... And miles to go before I sleep.

Domain-specific languages are quite important these days, and Groovy is suited extremely well for implementing them.
A DSL is a language that allows expressions in a domain-specific manner (you could be more precise and more theoretical, but let us be pragmatic). Examples for DSLs are music scores, chess moves, or expressions regarding time and duration or space and distance.

A Domain-Specific Language

Let us take the last example, and think about possible uses of working with distances. What we would like is something like this:
3.m + 2.yd + 2.ml - 1.km
Or like this:
3.mi > 3.km

And a notation to simply convert distances to different units would be nice:
3.mi.km
meaning 3 miles as kilometers.

The interesting point is that with Groovy, this is quite simple to implement.

The Plumbing

We need support for the notation .unit, we need support for plus() and minus() operations, and we have to support comparisons between different distances. This involves converting from one unit to others. And we need a Distance and a Unit type. Before you say, oh God, way too much, let me tell you that the total number of lines to implement this DSL is less than 90 lines of source code.

Let us start with the Distance type. We need a reference to a Unit object and a length, an implementation of the methods plus() and minus(), an implementation of the methode compareTo() (which implies that Distance has to implement the interface Comparable) and a toString()-method. The conversion of distances in different units is simply delegated to the Unit class, since that class should know best.
class Distance implements Comparable {
BigDecimal length
Unit unit

Distance plus(Distance operand) {
def newLength = this.length + Unit.convertUnit(operand, this.unit)
new Distance(length : newLength, unit : this.unit)
}
Distance minus(Distance operand) {
def newLength = this.length - Unit.convertUnit(operand, this.unit)
new Distance(length : newLength, unit : this.unit)
}
int compareTo(other) {
if(this.unit == other.unit)
return this.length <=> other.length
return this.length <=> Unit.convertUnit(other, this.unit)
}
String toString() {
"$length $unit.name"
}
}

Nothing special here.

Now our Unit class should contain some predefined Unit objects representing the supported units. We have already seen that the Unit object needs a name. Furthermore it gets a ratio compared to the meter.
In principle the ratio would be a single value and we could compute different unit simply by dividing and multiplying in the correct order. But we have the problem that values are represented with finite precision. So to get our conversion as precise as possible we define a table that allows direct conversion with only one operation instead of two or three.

The ratio now is the row index in the table. To convert between different unit in principle would simply mean to access the ratio table for the coefficient and then compute the conversion. Since the conversion coefficients for different directions of the same unit combinations (e.g. m -> mi and mi -> m) are reciprocal, the table contains the coefficient only for one direction and a 0 for the other direction. If we access the table and get a 0, we do the inverse operation with the other coefficient.
There might be better and easier to extend ways to implement the conversion, but this one is fairly simple.

class Unit {
def ratio
String name

static def convertUnit(Distance d, Unit newUnit) {
def factor = ratioTable[d.unit.ratio][newUnit.ratio]
if(factor)
return d.length * factor
else
return d.length / ratioTable[newUnit.ratio][d.unit.ratio]
}
static ratioTable = [
// mm, cm, m, km, y, mi
[ 1, 0, 0, 0, 0, 0 ], // mm
[ 10, 1, 0, 0, 0, 0 ], // cm
[ 1e3, 1e2, 1, 0, 0, 0 ], // m
[ 1e6, 1e5, 1e3, 1, 0, 0 ], // km
[ 914.4, 91.44, 0.9144, 0.9144e-3, 1, 0 ], // yd
[ 1.609344e6, 1.609344e5, 1.609344e3, 1.609344, 1760, 1 ], // mi
]

public static final mm = new Unit(ratio : 0, name : "millimeter")
public static final cm = new Unit(ratio : 1, name : "centimeter")
public static final m = new Unit(ratio : 2, name : "meter")
public static final km = new Unit(ratio : 3, name : "kilometer")
public static final yd = new Unit(ratio : 4, name : "yard")
public static final mi = new Unit(ratio : 5, name : "mile(s)")
}

Finally we define constants for all the units we support (mm, cm, m, km, yd, mi).

Now we have everything we need as plumbing. We can add and subtract different Distance objects using the + and - operator, we can compare distances with the <, > and == operators and we can convert different distances.

Creating the DSL

To create our DSL we use a category. For each of the supported units we implement the method get() for the types Number and Distance. Be defining the method for Number objects we can use the notation "3.2.mi", by defining the method for Distance objects as well we can convert on the fly using the notation "3.2.mi.km". The respective implementations are straightforward.

class DistanceCategory {
static Distance getMm(Number n) { new Distance(length : n, unit : Unit.mm) }
static Distance getMm(Distance d) {
new Distance(length : Unit.convertUnit(d, Unit.mm), unit : Unit.mm)
}
... (repeat for cm, m, km, yd, mi)
}

And that's it. We could also try to generalize the get()-methods, but for such a simple DSL this is not worth the time.

Using the DSL

Using the DSL is as simple as using any other category:
use(DistanceCategory.class) {
def d1 = 1.m
def d2 = 1.yd
def d3 = 1760.yd
def d4 = 100.cm
println d1 + 1.yd
println 1.yd + 1.mi
println 1.m - 1.yd
println d2.m
println d3.mi
println d4.m
println 1000.yd.km
println 1000.yd
The result of the different println statements is the following:
1.9144 meter
1761 yard
0.0856 meter
0.9144 meter
1 mile(s)
1 meter
0.9144000 kilometer
true
Cool, isn't it? In less than 90 lines of source code we have a full DSL for describing distances, for adding and subtracting them, and for comparing them.

Next steps could be to allow multiplication and division as well, but then it gets interesting. Imagine the product of 1.m, 2.yd and 3.mi.

Credits

I took the idea for this DSL from a talk given by Bernd Schiffer at the JFS 2007. Bernd, I hope you don't mind.

This blog's title is the last line of a poem by Robert Frost (in case you wondered).

5 comments:

Guillaume Laforge said...

For the unit conversion, I think you may have used 'as', it would have allowed you to write things like: 1.km as Miles.

Anonymous said...

very good example.
But tell me, can you separte the classes Distance and Unit ? the two classes must be in 1 file !!!

Joachim Baumann said...

You can place Distance, Unit, and DistanceCategory in different files without problems. You have to remember, though, that if you put the classes in different packages, then you have to import them using the import statement (as usual in Java).

Anonymous said...

yes the separation work. Now, il will be happy if we have an example with "as" to allow wrots like "1.km as miles"
Thank you

Justin said...

Great post. Using categories is a great way to increase a Grooy DSLs fluency.