Units of Measure in F#: Part Four, Parameterized Types
It's been a while since I wrote anything on this blog. Now is a good moment to continue, with last week's Beta 1 release of Visual Studio 2010, including F#, and a refresh to the F# Community Tech Preview add-on for VS 2008. If you haven't yet installed either of these, I recommend that you do. There have been many bug fixes and improvements to F# since last year.
Since the initial CTP release of F# I've been busy fixing some bugs that have been uncovered, and giving talks on the units feature. I presented at the ML workshop that was co-located with ICFP'08, the International Conference on Functional Programming. (For the theoretically minded, I also gave a talk at the Workshop on Mechanizing Metatheory, or WMM for short.) A couple of weeks ago I presented a lecture course on units-of-measure to PhD and masters students, at the Central European summer school on Functional Programming, CEFP'09. The lecture notes from that course will be peer-reviewed and published by Springer later this year.
Now back to the blog tutorial. In the first three parts of this series, we saw how to define units-of-measure, how to use units with the built-in floating-point type float, and how to write code that's generic in units-of-measure.
There are two other built-in types with this feature: float32 (the type of single-precision floating point) and decimal. Here's how constants of these types are introduced:
But what if I want to create my own types with units, for example, complex numbers, 3-vectors, or matrices? That's easy: just parameterize the type, just as you might parameterize a type such as list or Dictionary, but mark the parameter with the [<Measure>] attribute:
As you can see, units beget units: in the Sphere type, we've used the Vector3 type, which in turn used the built-in float type. All are parameterized on units-of-measure.
Now we can create a point and acceleration, both vectors, but with different units:
That was easy enough. Now let's define some operations on vectors:
Notice how F# inferred a generic type for dot product. Of course, we'd really like to extend operators such + and - with overloads for our new type. To do this, we can define static members on the type:
There are a couple of things to notice here. First, we've given the units of the arguments explicitly. If we don't do this, then F# will infer extra unit parameters rather than using the unit parameter 'u from Vector3 itself. Second, we have implemented various kinds of multiplication: multiplying a vector on the left and on the right by a scalar (i.e. a float), multiplying a vector by a vector to get the vector product, also known as the cross product, and finally, the dot product, which we've called '.*'. In order to define multiple overloads for *, we've used the OverloadID attribute to distinguish them. Now let's look at the signature that F# gives to this type, by hovering the cursor over the declaration:
We can now implement a "vector" version of Newton's second law (F = ma), multiplying a scalar mass by an acceleration vector to get a force vector:
The use of records in the Vector3 type is a traditional functional programming approach. A more .NET-oriented style of definition is to define a class with implicit constructor, instead of a record type. Here's a type for complex numbers defined this way, including instance members to obtain the polar representation of complex numbers as magnitude and phase, and to print complex numbers in the form a + ib:
One "gotcha" here is the need for explicit type signatures on the re and im properties. Without these hints, F# infers weaker types for arithmetic (try it!). Fortunately, this issue will be resolved in the final release of F#.
We can now do some electrics!
Executing this code in F# Interactive produces:
The amplitude of a voltage value has type float<V>, whilst the phase has type float, being measured in radians.
Summing up. We've seen how to parameterize user-defined types on units-of-measure, how to define members that make use of the unit parameters, and how to overload operators to define non-standard arithmetic.
Next time we'll have a look at the internals of type checking and type inference for units-of-measure. You don't need to know this to use the units feature, but you might be curious anyhow. For instance, however does F# figure out the type of the following?