The feeling that there is something wrong with the math in my code is always annoying, though. It turns out F# has a solution for that, called units of measure.
A unit of measure is a type annotation that helps programmers write correct formulas. It's obviously useful for formulas in simulations, whether one is dealing with physics, finances or any other domain where models play an important role.
The code below shows how to declare a unit measure.
/// Position, meters [<Measure>] type m /// Time, seconds [<Measure>] type s /// Mass, kilograms [<Measure>] type kgUnits in physics have a certain level of redundancy, one might say. For instance, forces can be expressed in Newtons or in kilograms time meters per squared seconds. Newtons are obviously more convenient to use, but you want that masses multiplied by accelerations be recognized as forces. This is how you capture Newton's law:
/// Force, Newtons [<Measure>] type N = kg m/s^2Units of measure can be used with primitive numeric types such as int, float and float32.
let integrateShips (dt : float32<s>) (ships : Ships) ...dt above denotes a time duration in seconds represented using a 32-bit floating point value. Units of measure can also be applied to complex types. The code below shows the code for a wrapper around Xna's Vector3.
/// A three-dimensional vector with a unit of measure. Built on top of Xna's Vector3. type TypedVector3<[<Measure>] 'M> = struct val v : Vector3 new(x : float32<'M>, y : float32<'M>, z : float32<'M>) = { v = Vector3(float32 x, float32 y, float32 z) } new(V) = { v = V } member this.X : float32<'M> = LanguagePrimitives.Float32WithMeasure this.v.X member this.Y : float32<'M> = LanguagePrimitives.Float32WithMeasure this.v.Y member this.Z : float32<'M> = LanguagePrimitives.Float32WithMeasure this.v.Z end [<RequireQualifiedAccessAttribute>] module TypedVector = let add3 (U : TypedVector3<'M>, V : TypedVector3<'M>) = new TypedVector3<'M>(U.v + V.v) let sub3 (U : TypedVector3<'M>, V : TypedVector3<'M>) = new TypedVector3<'M>(U.v - V.v) type TypedVector3<[<Measure>] 'M> with static member public (+) (U, V) = TypedVector.add3 (U, V) static member public (-) (U, V) = TypedVector.sub3 (U, V)This allows to add and subtract vectors with compatible units of measure. It took me some effort to figure out how to handle multiplication by a scalar. First, in module TypedVector:
let scale3 (k : float32<'K>, U : TypedVector3<'M>) : TypedVector3<'K 'M> = let conv = LanguagePrimitives.Float32WithMeasure<'K 'M> let v = Vector3.Multiply(U.v, float32 k) new TypedVector3<_>(conv v.X, conv v.Y, conv v.Z)Then the type extension:
type TypedVector3<[<Measure>] 'M> with static member public (*) (k, U) = TypedVector.scale3 (k, U)Note the use of LanguagePrimitives.Float32WithMeasure<'K 'M> to produce a number with a specific unit of measure in a generic fashion.
I have designed the class to reuse Xna's implementation although it wouldn't have been hard to write my own from scratch. The key benefit, on Windows Phone 7, is to take advantage of some fast vector math that's only accessible through Xna's types. The PC and Xbox platforms don't support fast vector math, but who knows, it may come.
Finally, here comes an example of how to use all this:
let speeds2 = Array.map2 (fun speed (accel : TypedVector3<m/s^2>) -> let speed : TypedVector3<m/s> = speed + dt * accel speed) ships.speeds.Content accels.Content let posClient = ArrayInlined.map3 (fun pos speed speed2-> pos + 0.5f * dt * (speed + speed2)) ships.posClient.Content ships.speeds.Content speeds2There is more to be said and written about units of measures, a future post will show how they can be used for safe access of array contents.