Units in Julia

I’ve been excited about Julia since before their first release (back then you had to build it from source). Lately I’ve been working on CGP.jl so I’ve been able to really immerse myself in the language and ecosystem.

I have always found the idea of associating physical units with values in a programming language to be interesting and potentially useful. We have languages with very powerful type systems, but we don’t typically have elegant ways of saying “this is a floating point value and it represents a number of centimeters”. These exist, of course, and implementing this kind of thing in an object oriented language is probably a pretty useful learning exercise, but again, it’s really about elegance and simplicity.

Julia has three characteristics that make something like this relatively elegant. First, it has optional static types and multiple dispatch. This should let us write a function that operates on “inches” but not on “centimeters” or “kilograms”. Second, it has functions as operators (and some nice syntactic sugar related to multiplication). This lets us override basic operations like addition. When combined with parametric types, we can even use a single implementation to handle multiple units. Finally, Julia supports rich Lisp-style (more or less) macros, meaning we can easily define a whole bunch of unit types and associated functions with a relatively small amount of code.

It is, perhaps, worth considering how we would do something like this in a functional language that supports pattern matching (since, to me at least, that would be the other obviously “elegant” way of solving the problem. Here’s a stupidly simple example in Elixir (which, by the way, also supports macros).

https://gist.github.com/glesica/51e2fa379e9c0105c302

Operator overloading is possible in Elixir, but the point is really that we can very cleanly implement a function that takes only particular kinds of units by agreeing to pass around tuples of a certain kind. We don’t have this sort of pattern matching in Julia, but we can match on types (multiple dispatch).

So here’s the code in Julia:

https://gist.github.com/glesica/ccb86f4d5ad3eb076d58

Let’s look at it a piece at a time. We’ve got three macros. Let’s take them in order. The first, defunit allows us to add a new unit to the units graph we will create. It requires a name and a parent (or “kind”). The name serves an obvious purpose, the parent is less clear. This macro also defines a shortcut function that lets us convert another unit of the same kind (sharing a parent) to this one. So we can do things like In(x) where x is a Cm value.

Each kind of unit has a base unit. This is the unit through which all conversions that aren’t specifically specified will be done. We can define a base unit for a kind using the defbase macro. For example, in the snippet we define centimeters to be the linear base unit. This means that to define an inches unit we need only provide a conversion to centimeters to be able to convert between inches and any other linear unit (such as feet, in the example). This doesn’t work perfectly since we might end up with significant floating point error or overflow, but it works well enough.

Last of all, we can define a conversion using the defconv macro. We must define a conversion from a unit into the base unit, but we can also define other conversions if we’d like better accuracy.

Next, let’s take a look at this line: *{T <: Unit}(x::Real, ::Type{T}) = T(x). Here we have abused the multiplication operator and made it into a pseudo constructor. Why? Because Julia has some fancy syntactic sugar that lets us write 2x instead of 2 * x. This was added, presumably, to aid in translating mathematical formulas to code. For our purposes, it allows us to write something like 2Cm and have it mean exactly what it looks like it should mean. We need to be careful about operator precedence of course.

Finally, we define a bunch of arithmetic functions / operators. Since all units have more or less the same form, what we're doing here is enforcing the rule that you can only multiply a value of a particular unit by another value of the same unit. This means, then, that something like 2Cm + 4Ft is an error (since who knows what the resulting unit should be?! To make this work we would need to explicitly acknowledge the units by converting one of them like so: 2Cm + Cm(4Ft).

I generally like this solution to the problem. Julia provides all the tools necessary to solve this problem in a fairly elegant manner.

Image credit: Scott Akerman

Advertisements