Wide Awake Developers

« Kudos to Relevance and Clojure | Main | Minireview: Beginning Scala »

Units of Measure in Scala

Failure to understand or represent units has caused several major disasters, including the costly Ariane 5 disaster in 1996. This is one of those things that DSLs often get right, but mainstream programming languages just ignore. Or, worse, they implement a clunky unit of measure library that ensures you can never again write a sensible arithmetic expression.

While I was at JAOO Australia this week, Amanda Laucher showed some F# code for a recipe that caught my attention. It used numeric literals with that directly attached units to quantities. What's more, it was intelligent about combining units.

I went looking for something similar in Scala. I googled my fingertips off, but without much luck, until Miles Sabin pointed out that there's already a compiler plugin sitting right next to the core Scala code itself.

Installing Units

Scala has it's own package manager, called sbaz. It can directly install the units extension:

sbaz install units

This will install it under your default managed installation. If you haven't done anything else, that will be your Scala install directory. If you have done something else, you probably already know what you're doing, so I won't try to give you instructions.

Using Units

To use units, you first have to import the library's "Preamble". It's also helpful to go ahead and import the "StandardUnits" object. That brings in a whole set of useful SI units.

I'm going to do all this from the Scala interactive interpreter.

scala> import units.Preamble._
import units.Preamble._

scala> import units.StandardUnits._
import units.StandardUnits._

After that, you can multiply any number by a unit to create a dimensional quantity:

scala> 20*m
res0: units.Measure = 20.0*m

scala> res0*res0
res1: units.Measure = 400.0*m*m

scala> Math.Pi*res0*res0
res2: units.Measure = 1256.6370614359173*m*m

Notice that when I multiplied a length (in meters) times itself, I got an area (square meters). To me, this is a really exciting thing about the units library. It can combine dimensions sensibly when you do math on them. In fact, it can help prevent you from incorrectly combining units.

scala> val length = 5*mm
length: units.Measure = 5.0*mm

scala> val weight = 12*g
weight: units.Measure = 12.0*g

scala> length + weight
units.IncompatibleUnits: Incompatible units: g and mm

I can't add grams and millimeters, but I can multiply them.

Creating Units

The StandardUnits package includes a lot of common units relating to basic physics. It doesn't have any relating to system capacity metrics, so I'd like to create some units for that.

scala> import units._
import units._

scala> val requests = SimpleDimension("requests")
requests: units.SimpleDimension = requests

scala> val req = SimpleUnit("req", requests, 1.0)
req: units.SimpleUnit = req

scala> val Kreq = SimpleUnit("Kreq", requests, 1000.0)
Kreq: units.SimpleUnit = Kreq

Now I can combine that simple dimension with others. If I want to express requests per second, I can just write it directly.

scala> 565*req/s
res4: units.Measure = 565.0*req/s

Conclusion

This extension will be the first thing I add to new projects from now on. The convenience of literals, with the extensibility of adding my own dimensions and units means I can easily keep units with all of my numbers.

There's no longer any excuse to neglect your units in a mainstream programming language.

Comments

I had to make some tweaks to my sbaz install to download units.

My universe was set to lamp-rc rather than scala-dev. Units is available from scala-dev. To update it I went to the directory where my sbaz descriptor files are on my system:

cd /opt/local/share/scala/misc/sbaz/descriptors

I then set my universe to scala-dev:

sudo sbaz setuniverse scala-dev

Units was then listed as available.

% sbaz available|grep units
units (1.5, 1.4, 1.3, ...)

Do you allow conversions for things like different temperature scales, and if so, how do you deal with deltas?
If I want to express something like (10°C + 5°C) I would be suprised to get 15°C instead of 288.15°K.
Is there some kind of way that the system infers that 5°C is a delta temperature instead of an absolute temperature, or some way to specify that?

Are units statically checked?
If so, how do I specify the type of an operation like Math.pow where it only makes sense to allow either:
1. A scalar raised to any scalar power: Math.pow(4, y)
2. A non-scalar raised to a constant scalar power such that the resulting powers are integral: Math.pos(2*m*m, .5) but not Math.pow(2*m*m, .25)

Can unit types be used generically, as in generic vector or matrix operations?

cheers,
mike

Mike,

First off, I want to be clear that this isn't my library. I wrote up this post because it's a cool library and I couldn't find any other mention of it.

As far as I can tell, units.StandardUnits only contains SI units. So, while it can convert from grams to milligrams to yottagrams (I'm not making that up!), there don't appear to be conversions between different scales for the same type of dimension.

Units are statically checked. To me, that's part of the coolest thing about this library. So, for example, I just made a quick file "unit_example.scala":

import units.Preamble._
import units.StandardUnits._

object UnitExample extends Application {
2*kg + 2*m
}

Adding meters and kilograms doesn't make sense, and sure enough, this file won't compile. I get "error: incompatible units (Mass vs. Length)" when I compile that code. Multiplying the quantities would be allowed.

Powers are a tougher issue. You can multiply a quantity times itself, and that will respect the unit. Passing a quantity to Math.pow() or Math.sqrt(), though, results in an error because Scala can't find a suitable overload of the method. (And, I guess implicit conversion fails too.)

-Mike

This looks interesting. Who is the actual author of this?

It is missing some units and more complex operations are't seen either, but it is an elegant approach.

I tried to add lenght and mass, but I only got a runtime exception in the Scala IDE.
Compared to e.g. JSR-275, the standard library for Measures and Units it has no compile time checking, but it is a little shorter in its notation.

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)