Ruminations on a system of units of measure in Scala, Part 3: Under the hood of the static implementation

Welcome to part 3 of the ongoing series discussing my implementation of units of measure in Scala. The good news is that this is the last part detailing the implementation – once we’ve got this out of the way I can start getting on to what I’ve learned from the process. If you haven’t read parts 1 and 2 yet, read them first, as otherwise this will make very little sense.

Are you all caught up? Good. I’ll continue.

The core concepts behind the static implementation are very similar to those of the dynamic implementation. The core difference is that there’s no runtime representation of dimensions any more: instead, that’s handled by the type system. For reference, this is what the swimming pool program looks like with the statically typed implementation of the library:

package com.writeoncereadmany.unitsofmeasure.statictypes.samples

import com.writeoncereadmany.unitsofmeasure.statictypes.units.Volume._
import com.writeoncereadmany.unitsofmeasure.statictypes.units.Length._
import com.writeoncereadmany.unitsofmeasure.statictypes.units.Sterling._
import com.writeoncereadmany.unitsofmeasure.statictypes.conversions.LengthConversions._
import com.writeoncereadmany.unitsofmeasure.statictypes.conversions.SterlingToVolumeComversions._

object StaticSample
{
  def main(args: Array[String])
  {
    val length = 50 metres
    val width = 25 metres
    val depth = 9 feet
    val swimmingPool = length * width * depth

    val costOfAPint = (3.50 pounds) / (1 pints)

    println(f"A swimming pool full of beer would cost £${(swimmingPool * costOfAPint) in pounds}%10.2f")
  }
}

It’s almost identical to the implementation using the dynamic library, only with a couple more imports.

Just like the dynamic implementation, the static implementation has classes called Quantity and UnitOfMeasure. The difference here is that Quantity is generic and abstract, and UnitOfMeasure is generic. The objective here is to catch dimension mismatches with the type system, rather than at runtime – telling you immediately when you’ve made a dimensionality error. To implement a new dimensionality, therefore, you need a new type: a new implementation of Quantity. For example, Length and Area are different classes, both of which extend Quantity.

Therefore, we don’t have any analogue of Dimension and Dimensions. These no longer exist as concerns: they’ve been rolled into the type system.

Let’s take a quick look at Length:

package com.writeoncereadmany.unitsofmeasure.statictypes.units

import com.writeoncereadmany.unitsofmeasure.statictypes.{Quantity, UnitOfMeasure}

class Length(value: Double, unit: UnitOfMeasure[Length]) extends Quantity[Length](value, unit)
{
  def newInstance(unscaledValue: Double) = new Length(unscaledValue, Length.metres)
}

object Length
{
  implicit def double2LengthBuilder(value: Double) = new LengthBuilder(value)

  val metres = new UnitOfMeasure[Length](1)
  val kilometres = (1000 metres) asNewUnit
  val centimetres = (0.01 metres) asNewUnit()
  val feet = (0.3084 metres) asNewUnit()
}

class LengthBuilder(value: Double)
{
  def metres = new Length(value, Length.metres)
  def kilometres = new Length(value, Length.kilometres)
  def centimetres = new Length(value, Length.centimetres)
  def feet = new Length(value, Length.feet)
}

We’re no longer referencing higher-order lengths (areas, volumes etc) – these now belong to their own types.

We’ve still got our object Length and our LengthBuilder class. Length still contains the various unit of measure definitions, and it still contains the implicit conversion to the builder class. The builder class looks very similar to that of the dynamic implementation, too. But on top of that, we have some new things, the most obvious being a Length class.

Length extends Quantity[Length]. Quantity provides implementations of addition and subtraction of like types (so you can add a Length to a Length, but you can’t add an Area to a Length) and multiplication and division by dimensionless numbers. Length implements one method: newInstance, which creates a new Length with a provided base value, which is how we support creating an instance of the correct class when doing addition/subtraction.

We’ve lost a slight bit of expressiveness: we can’t create a new Quantity from a UnitOfMeasure, as UnitOfMeasure is generic and isn’t aware of its dimensions – and thus type of quantity – at runtime, so it wouldn’t know what constructor to call. No huge loss, as calling the appropriate constructor in our builder isn’t much less clear than our previous application of the unit of measure.

No, the main way we hit pain is in converting between dimensions. Whereas the dynamic implementation would handle all the dimensional concerns for us, here we don’t only have to define each combination of dimensions we want to use – we also need to define how to handle each possible conversion between dimensions we want. Whereas we could handle this in the dynamic implementation at runtime by performing arithmetic operations on Dimensions, now we need to define the type interactions with a method for each conversion we want to support.

Take a look at LengthConversions:

package com.writeoncereadmany.unitsofmeasure.statictypes.conversions

import com.writeoncereadmany.unitsofmeasure.statictypes.units.{Volume, Length, Area}
import Length._
import Area._
import Volume._

object LengthConversions
{
  implicit def lengthToLengthConverter(length: Length) = new LengthConverter(length)
  implicit def areaToAreaConvertor(area: Area) = new AreaConverter(area)
}

class LengthConverter(val length: Length)
{
  def *(other: Length) = (length in metres) * (other in metres) squareMetres
  def *(other: Area) = (length in metres) * (other in squareMetres) cubicMetres
}

class AreaConverter(val area: Area)
{
  def *(other: Length) = (area in squareMetres) * (other in metres) cubicMetres
}

That doesn’t look too bad, does it? Well, no, but that’s because I’ve only implemented the operations strictly required for this trivial example (and note that LengthConversion alone isn’t enough to implement my swimming pool example – I also need SterlingToVolumeConversions).

I could have defined these operations on the concrete dimension classes (eg class Length), but whilst it’s arguable that a Length should know that when it’s multiplied by a Length you get a Volume, it’s harder to argue that a Volume should know enough about money to know that when it’s multiplied by a SterlingPerVolume we get a Sterling.

The pain hits on the end-user level too. It’s easy enough to realise that when you want to deal with lengths you import Length._, but working out what you need to import when you’re converting between money and volume and whatever is less obvious.

On the other hand: don’t underestimate the value of type safety. It’s worth jumping through some hoops to detect your errors at compile time instead of runtime.

Which approach is better – the freedom of dynamic typing, or the cruft/safety tradeoff of static typing? I can see the benefits of both. I can also see a better way than either, and that’s what this series is working towards.

Advertisements

2 thoughts on “Ruminations on a system of units of measure in Scala, Part 3: Under the hood of the static implementation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s