Ruminations on a system of units of Measure in Scala, Part 2: Under The Hood of the Dynamic Implementation

So I’m talking about my implementation of a system of units of measure in Scala. You might want to read part 1 first, so you’ve got an idea of what I’m trying to achieve and why you might want to extend this.

There are four primary classes at work here (none of which an end-user should need to be directly aware of) – Dimension, Dimensions, UnitOfMeasure, and Quantity.

Dimension is a marker trait: when establishing a new dimension (such as length, charge, force etc), you’ll need an object which extends this trait. It’s recommended that this is a case object. There are various other concerns about this dimension object, but we’ll cover those later once we’ve established more context.

Dimensions represents the dimensionality of a given quantity: Length^1 for distance, Length^2 for area, Length^1*Time^-1 for speed, Length^1*Time^-2 for acceleration, and so on. It’s represented internally by a map of Dimension to power: the dimensions of acceleration, for example, could be created with new Dimensions(Map(Length->1, Time->-2)).

For a given dimension, there’s a given base unit. For example, when talking about lengths, a sensible base unit might be the metre. As all quantities are represented as Doubles, in the base quantity, choosing a base unit which is orders of magnitude away from the values you’re likely to use (eg using lightyears or Planck-lengths when designing breadboxes) is going to lead to some accuracy issues. Of course this wouldn’t be an issue if we were using a sensible number representation, but that’s an argument for a past post.

Once you’ve decided on your base unit, then you can define a unit of measure for it. A unit of measure has a scaling factor (defined as the number of base units of that dimension which amount to one of that unit of measure), and a dimensionality. For example, a kilometre is 1000 metres: that’s 1000 * base unit for Length, with the dimensionality of Length^1.

So for the base unit of length – a metre – we can define a unit of measure as follows:

val metres = new UnitOfMeasure(1, Dimensions(Map(Length->1)))

Similarly, we could define a kilometre as follows:

val kilometres = new UnitOfMeasure(1000, Dimensions(Map(Length->1)))

There’s an easier way, but we’ll get to that in a minute.

The last piece of the puzzle is Quantity, which is a number of units of measure. Well, that’s how you create a Quantity:

val myHeight = new Quantity(1.83, metres)

Internally, it’s stored in base value (ie, each dimension scaled to its base unit) and dimensions. But you don’t need to know that – it’s equivalent to being stored as a number of whatever units of measure you provided. Quantities support the basic mathematical operators – you can add or subtract them (if both quantities have the same dimensions – otherwise, you get a DimensionMismatchException), and you can multiply and divide them (and it derives the dimensions of the result). If you import support for dimensionless quantities (ie numbers), you can multiply and divide by them too.

You can’t get a numeric value out of a Quantity without providing a UnitOfMeasure (which, of course, has to match the dimensions of the quantity, or you get a DimensionMismatchException). So, there’s no chance of confusing millimetres and inches and ending up with a Jimbo Jet – you can’t turn Quantities back to numbers without explicitly stating your assumptions about units, and if your assumptions change, then it transparently converts between contexts.

Generally, although an end-user will interact only ever be aware of Quantity as a class, when defining units/dimensions you can ignore Quantity as a class. Let’s take a quick look at the file defining units of time:

package com.writeoncereadmany.unitsofmeasure.dynamictypes.units

import com.writeoncereadmany.unitsofmeasure.dynamictypes.{Dimensions, UnitOfMeasure, Dimension}

case object Time extends Dimension
{
  implicit def double2TimeBuilder(value: Double) = new TimeBuilder(value)

  val seconds = new UnitOfMeasure(1, new Dimensions(Map(Time->1)))
  val minutes = (60 seconds) asNewUnit
  val hours = (60 minutes) asNewUnit
  val days = (24 hours) asNewUnit

}

class TimeBuilder(val value: Double)
{
  def seconds = Time.seconds(value)
  def minutes = Time.minutes(value)
  def hours = Time.hours(value)
  def days = Time.days(value)
}

The first thing to point out is the definition of a base unit:

val seconds = new UnitOfMeasure(1, new Dimensions(Map(Time->1)))

This is our starting point for all other units of measure: by convention, there should only ever be one unit which uses the UnitOfMeasure constructor for a given dimension, and it should have a scaling factor of 1.

The next thing to point out is the method double2TimeBuilder. This is an implicit method which can turn Doubles into TimeBuilders – this is what allows us to make statements like:

val worldRecord = 9.86 seconds

instead of having to make statements like:

val worldRecord = seconds(9.86)

The latter syntax is supported – UnitOfMeasure has an apply() method which takes a value and turns it into a Quantity. However, the former syntax is much more natural, and supporting it is simply a case of implementing a class Builder, taking a value, with a method for each unit applying the given quantity to the given unit.

Once you’ve established that pattern, defining new units is simply a case of stating what the units should be in terms of previously defined units. Quantity has a method asNewUnit() which allows us to make statements like so:

val kilometres = (1000 metres) asNewUnit

As long as you define a new method on the builder for each unit, it’s easy to continually define units in terms of other units – minutes in terms of seconds, hours in terms of minutes and so on. By defining all the units and the implicit conversion method within a given Dimension object, we only need to import ._ to put both all the units and the conversion to allow declaration in units in scope – that’s all the end-user needs.

If you want to add units to a pre-existing Dimension – for example, to use imperial lengths when metric lengths have already been defined – don’t define a new Dimension; instead, piggyback on the existing Dimension. For example, see Length and ImperialLength:

package com.writeoncereadmany.unitsofmeasure.dynamictypes.units

import com.writeoncereadmany.unitsofmeasure.dynamictypes.{Dimensions, UnitOfMeasure, Dimension}

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

  val metres = new UnitOfMeasure(1, new Dimensions(Map(Length->1)))
  val kilometres = (1000 metres) asNewUnit
  val centimetres = (0.01 metres) asNewUnit


  // higher order units: areas
  val squareMetres = (1 metres) * (1 metres) asNewUnit

  // higher order units: volumes
  val cubicMetres = (1 metres) * (1 squareMetres) asNewUnit
  val litres = (0.001 cubicMetres) asNewUnit
}

class LengthBuilder(val value: Double)
{
  // lengths
  def metres = Length.metres(value)
  def kilometres = Length.kilometres(value)
  def centimetres = Length.centimetres(value)


  // higher order units: areas
  def squareMetres = Length.squareMetres(value)

  // higher order units: volumes
  def cubicMetres = Length.cubicMetres(value)
  def litres = Length.litres(value)
}
package com.writeoncereadmany.unitsofmeasure.dynamictypes.units

import com.writeoncereadmany.unitsofmeasure.dynamictypes.units.Length._

case object ImperialLength {
  implicit def double2ImperialLengthBuilder(value: Double) = new ImperialLengthBuilder(value)

  val feet = (0.3084 metres) asNewUnit
  val inches = (1/12.0 feet) asNewUnit
  val yards = (3 feet) asNewUnit

  // higher order units: volume
  val pints = (0.568 litres) asNewUnit
  val gallons = (8 pints) asNewUnit
}

class ImperialLengthBuilder(val value: Double)
{
  def feet = ImperialLength.feet(value)
  def inches = ImperialLength.inches(value)
  def yards = ImperialLength.yards(value)

  // higher order units: volume
  def pints = ImperialLength.pints(value)
  def gallons = ImperialLength.gallons(value)
}

Note that ImperialLength doesn’t extend Dimension, and doesn’t define base units: that’s because its dimension is Length, and it takes its base units from Length. By defining different systems of units which represent the same dimension in terms of each other, you retain interoperability between different systems of units with transparent conversions.

And that’s about all you need to know about the dynamic implementation. Coming in part 3: how to extend the static implementation.

Advertisements

3 thoughts on “Ruminations on a system of units of Measure in Scala, Part 2: Under The Hood of the Dynamic 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