In a previous post, I discussed static abstracts in interfaces, and while it is quite a nice feature, it falls short in some corner cases – particularly in implicit type-conversions. I’m not sure if it should, but… it’s complicated. As an example, I will consider the concept of interchangeable units.
Wouldn’t it be neat, if we could create programs that operate on money, but where we never have to worry about the currency? or, if we have programs working with distance, but we do not have to worry about whether a function operates on feet or centimeters – and we could use yards and kilometers interchangeably? Then we might not crash any more space ships!
Consider a currency-example involving US Dollars, Swedish kroner, and Danish kroner; abbreviated USD, SEK and DKK respectively. We could of course create types for each, and do a conversion. We could even to implicit conversion, but we can only go so far… even in C#11 with static abstract interface members and in particular operators. It is not perfect, but it is quite good:
Case: Currencies in C#11 (current preview)
We can create programs where we mix currencies without worrying:
This example would first print the exchange rates:
ExchangeRates: (USD)100: (DKK)700.00 (SEK)100: (DKK)75.00 (NOK)100: (DKK)70.00 ---Examples--- (DKK)14.00 (USD)4.00 (SEK)14.00 (DKK)10.50 (DKK)52.50
And no matter how we write our programs, we will always get the correct value in any currency! But how can we do this?
Basic idea, core abstractions and Lenght-units
The core of the problem is to abstractly describe a class hierarchy with a well defined universal conversion mechanism. For distances and currencies, the conversion is linear, so we can convert between any currency or between any distance unit using a factor. We introduce three abstractions:
ILinearlyConvertibleHierarchy and two versions of
ILinearlyConvertibleHierarchyinterface provides the base of the hierarchy, and most importantly, a generalized conversion between everything in the hierarchy to the baseline value.
IDerivative<TBase>; Most importantly, the basic property of derivatives is the Factor (or exchange rate in currencies) to the baseline value, hence the simple derivative-type
IDerivative<TBase>— which is probably unnecessarily constrained in the example. At this abstraction level we know nothing but the this Factor.
- IDerivative<TBase,TDerivative> extends its base interface with static abstract operator overloads for implicit conversion
This class diagram gives an overview of the interfaces (including sub-interfaces created later):
We can then create a baseline value implementation from the above, and define a new helper abstraction for defining derivatives of the baseline value. Here we define the SI-unit Meter (M in the example), along with a few trivial derivatives CM, KM, but also the perhaps less obvious USMile:
Although the basic abstractions look complicated, the units themselves are quite simple. The XUnit-tests excerpts below show the expected behaviour:
The class diagram below reveals, that even though we speak of base and derivatives, there are no class hierarchy as such, only different classes implementing the interfaces:
Extending with operators: Currency
We can also use this to define currencies and derivatives. Lets define Danish Kroner:
Instead of being stuck on doubles, we added a type-parameter, TValue, to all the interfaces. TValue has the constraint that TValue : INumber<TValue> in order to switch to decimal ( or any number type! ). We also added Change helper methods, and an implementation of a plus operator for DKK. In the base interface for DKK-Based types, we added an abstract + operator, which makes the entire hierarchy subject to addition.
The new version of ILinearlyConvertible is included here for completeness:
We can now create USD, SEK as derivatives of DKK:
With these implementations, can mix currencies almost as much as we want to. We can write:
SEK sek = new SEK(10); USD usd = new USD(10); NOK nok = new NOK(10); DKK v = usd + sek; v = sek + sek; v = usd + usd; v = v + sek; v = sek + v; v = sek + usd; sek = v + v; sek = sek + usd; sek = nok + usd;
And it will compile, and work as expected. Great huh?
Where it fails
It all looks great on paper, even though, we are forced to be a little verbose at times. One big part is missing though. We cannot do even the simplest assignments:
sek = usd will fail –though sek = (DKK)usd will not
OperateOnUSD(sek) will fail –though OperateOnUSD((DKK)sek) will not
This could be fixed by relaxing the restrictions some of the restrictions on implicit conversion implementations. Specifically, if we could convert from interfaces and not just concrete types. But this would be most easily solved in a scenario where we could create generic implicit conversions:
static TDerivative implicit operator <TOtherDerivedClass>(TOtherDerivedClass v) where TOtherDerivedClass : IDerivative<TBase, TOtherDerivedClass> => (TBase)v;
but even without, I think there would be ways to circumvent the needs for the above, by allowing conversions to and from interfaces, or to and from base-classes. In the latter case, by separating the value and factor into a non-generic object, from which we then allow conversions from. Without this, it is hard to see how we could solve the problems above in a static type-safe manner.