10.m -- distance in meters
10.sec -- duration in seconds
1000.m `per` 60.min == 1.kmh -- velocity
Enhancing the DSL for Type Safety
In "A mini DSL" we have used the Int
type to represent a millimeter
in our DSL of measurements. That was nice to begin with but also somewhat limiting since we
cannot use the Int
type again for other purposes and still expect type-safe expressions.
Let’s see how we can improve the situation.
The New Goal
We would still like to use the DSL to work for units of distance as before but also for units of duration. The combination of both naturally leads to units of velocity.
The type system shall disallow improper operations like adding a duration to a distance.
The implementation strategy is to first (intrusively) refactor the old solution to a point where it still works exactly like before but allows adding durations as a non-intrusive increment.
Step 1: Refactoring
In the old code, we made Int
an instance of the Millimeter
type class. We will now
generalize and make Millimeter
an instance of the Num
type class such that we can
do arithmetic operations on it.
data Millimeter = MM Int
instance Num Millimeter where
zero = MM 0
one = MM 1
(MM a) + (MM b) = MM (a+b)
(MM a) - (MM b) = MM (a-b)
(MM a) * (MM b) = MM (a*b)
MM a <=> MM b = a <=> b
hashCode (MM a) = hashCode a
fromInt a = MM a
fromInteger a = MM a.fromIntegral
This means that we can now calculate in terms of millimeters:
MM 10 + MM 10 == MM 20
For making 10.mm
the value MM 10
, we just make our Metric
type class
using Millimeter
as the return type instead of Int
.
class (Integral a) => Metric a where
mm :: a -> Millimeter
cm :: a -> Millimeter
m :: a -> Millimeter
instance Metric Int where
mm i = MM i
cm i = i.mm * 10
m i = i.cm * 100
And voilà, after the refactoring all old code still works like before.
main args = do
println $ 10.m - 20.cm + 10.mm - 3.cm == 9780.mm
So why the effort?
-
We get a new level of type safety.
-
We can now add new units of measurement.
Step 2: Adding Durations
The next step is to apply a non-intrusive increment where we do not touch exiting code at all but add the new feature of calculating with durations. We follow the same strategy as before.
data Seconds = SEC Int
instance Num Seconds where
zero = SEC 0
one = SEC 1
(SEC a) + (SEC b) = SEC (a+b)
(SEC a) - (SEC b) = SEC (a-b)
(SEC a) * (SEC b) = SEC (a*b)
SEC a <=> SEC b = a <=> b
hashCode (SEC a) = hashCode a
fromInt a = SEC a
fromInteger a = SEC a.fromIntegral
class (Integral a) => Time a where
sec :: a -> Seconds
minutes :: a -> Seconds
h :: a -> Seconds
instance Time Int where
sec i = SEC i
minutes i = i.sec * 60
h i = i.minutes * 60
main args = do
println $ 3.sec + 1.h - 30.minutes == 1803.sec
Let’s see what this gives us.
Putting it all together
With this new structure, we have some serious benefits.
-
We can no longer accidentally write
10.m + 10.sec
since the compiler rejects that withType error in expression "sec 10". Type is Seconds used as Millimeter
. Now, that is a nice and telling error message! Is Frege reading my mind? -
We can still mix
Int
values into the calculation and they will be safely promoted to the right type!10.mm + 3 == 13.mm
is just as correct and type-safe as10.sec + 3 == 13.sec
. This is controlled by thefromInt
implementation of theNum
type class.
And now we can go even further and mix distances and duration to describe velocity.
data Velocity = KMH Double
derive Eq Velocity
class (Real a) => Unit a where
kmh :: a -> Velocity
instance Unit Double where
kmh val = KMH val
per (MM mm) (SEC sec) =
KMH (( mm.fromIntegral / sec.fromIntegral) * 0.0036)
main args = do
println $ (500.m * 3) `per` (3000.sec + 600) == 1.5.kmh
Our DSL is really shaping up nicely. It supports us with
-
full type safety,
-
full type inference, and
-
excellent error messages.
It is still easy enough to read and write.
The main workhorses for creating the DSL were type classes in combination with the dot-syntax. We mainly did non-intrusive increments with one intrusive refactoring in-between.
All modifications were robust:
-
non-intrusive increments are robust by definition and
-
the type system supported us to do a robust refactoring when needed.