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.kmOr like this:
3.mi > 3.km
And a notation to simply convert distances to different units would be nice:
3.mi.kmmeaning 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 .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
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
Using the DSL
Using the DSL is as simple as using any other category:use(DistanceCategory.class) {The result of the different println statements is the following:
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
1.9144 meterCool, 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.
1761 yard
0.0856 meter
0.9144 meter
1 mile(s)
1 meter
0.9144000 kilometer
true
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:
For the unit conversion, I think you may have used 'as', it would have allowed you to write things like: 1.km as Miles.
very good example.
But tell me, can you separte the classes Distance and Unit ? the two classes must be in 1 file !!!
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).
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
Great post. Using categories is a great way to increase a Grooy DSLs fluency.
Post a Comment