I did just this very thing in C# a few months ago[1]. The problem I had was that it gets out of hand very quickly. I wanted to be able to divide distance by time and get speed out of it, but it is extremely difficult to satisfy all of the M-to-N relationships between types and still end up being usable.
I ended up abandoning it for the project I'm building, as I wasn't too clear on what the type of a secant of an angle in degrees should be and nobody on the interwebs seemed to care enough to take notice or answer my questions.
They couldn't do it. The compiler does static verifications that, say, a value of type Meter when divided by a value of type second results in a value of type Meter/Second, where '/' is a custom concept built into the F# compiler. The .NET runtime knows nothing about these custom types.
Yeah, that's pretty much exactly the issue I ran into. The problem is that the standard .NET type semantics can only infer "up" the inheritance hierarchy.
I had gotten really close. I had types like
Base
Length: Base
Meters: Length
Feet: Length
Compound<Base, Base>
Area<T>: Compound<T, T> where T: Length
SquareMeters: Area<Meters>
SquareFeet: Area<Feet>
and had defined all of my math as generic extension methods of the Base class so that the types would compose:
Compound<T, U> Multiply<T, U>(T a, U b) where T : Base where U : Base
T Divide<T, U>(Compound<T, U>, U b) where T : Base where U : Base
That went a long way towards getting fairly concrete types out of only a few lines of code. But I couldn't get it the rest of the way. C# explicitly forbids user-defined typecasts between types in an inheritance chain with each other, so while multiplying two Meters got me a Compound<Meters, Meters>, it was not then possible to take a Compound<Meter, Meter> and convert it to anything like Area<Meters> or SquareMeters automatically.
Interesting problem! I was wondering why even define things like SquareMeters, but as you say the issues is reducing the terms that have different forms but mean the same thing.
Having a SquareMeters type was meant to reduce complexity. Instead of always dealing with Area<Meters, Meters> all the time, it was meant to be a shortcut.
Hrm, what about namespace aliasing? "using SquareMeters = Composite<Meters, Meters>"? Since the problem is not the composition of the type but the verbosity of the deeply composited types, then perhaps it's just about making a shortcut for the name.
Okay, I've definitely convinced myself there is no good way to do this without having to write way too much code over and over again. The problem is basically equivalent to inserting items into a sorted list, but you only have one operation in which to do it. So, you are either limited to simple types, or your code for operations on types explodes trying to define all of the ways to compare types (in a generic sense) and different scenarios for combinations of types.
Is that a real issue? They could generate struct types, but why take the performance loss? It only affects people trying to reflect over the unit of measurement types.
Additionally, phantom types in F# can make wrapping other types (like int -> AccountId) rather succinct.
Ah, I see. Looking at how something similar was implemented in Haskell, they used phantom types and functional dependencies (in the type system) to achieve this - I'm not sure F# has those capabilities.
F# has that capability, but .NET does not, so once the F# compiler has done its checking, it drops the info on the floor and you have nothing at runtime.
Now, this might not necessarily be a bad thing for F# (I don't know, I'm not an F# user). The runtime type information in .NET is great, but it's all for reflection to be able to build things that a stronger type system like F# has, or a macro system, would be able to handle.
I ended up abandoning it for the project I'm building, as I wasn't too clear on what the type of a secant of an angle in degrees should be and nobody on the interwebs seemed to care enough to take notice or answer my questions.
[1] https://github.com/capnmidnight/UnitsOfMeasure