Typesystem

October 22, 2015

The previous post had a video on “Domain modelling with the F# Typesystem”

I’d like to elaborate how the insights gained there, impacted the way I was able to do a refactoring soon after at work.

What’s a typesystem good for?

The typesystem is the thing that complains when you pass a Double to a function that wants a Decimal. So then you have cast your Double to a Decimal and everything is fine again.

If this has been your experience with -and expectation of- typesystems, you’re missing out on a lot of really nice things they can do for you.

Calculate all the things

At work I encountered a chunk of legacy code that had been adapted by many people on many occasions. This module consisted of many methods all playing a role in calculating the return on investment of installing solar panels. There were tests, so we knew it was correct, but the original design had been eroded by the force of change.

The example code has been simplified a lot to make the point without adding distraction.

public Decimal OwnProduction(Decimal annualProduction,
                             Decimal electricityPrice)

Mystery abound

There’s a lot of implicit knowledge here which will result in bugs when you get them wrong. What is the unit of annualProduction, kWh I guess? Is electricityPrice tax included or not? What is OwnProduction even? kWh, Money? (turns out it was money saved)

Decimals, Decimals, Decimals

There were a lot more numbers involved and they were all Decimals. It was really easy to accidentally mix them up. So, instead of everything being a Decimal, let’s define some more specific types:

  • ElectricityAmount
  • MoneyAmount

These are value types. Meaning that objects of these types are immutable and that two different instances which represent the same amount, will be equal.

new ElectricityAmount(2000) == new ElectricityAmount(2000);
// true

Now we’ll apply these types throughout the calculations. for both types there’s an automatic conversion to Decimal, but not from Decimal. So we can gradually replace types without breaking the code and tests. When the refactoring is done, we’ll remove the automatic conversion and leave only explicit conversion to Decimal.

public MoneyAmount OwnProduction(ElectricityAmount annualProduction,
                                 MoneyAmount electricityPrice)

Taxes

In these calculations, MoneyAmounts could either be pre tax or post tax. For historic reasons both were used. Often you had to go read the implementation of a method to know which one was expected. Wouldn’t it be nice if the mixing up pre tax and post tax resulted in a compile time error?

Let’s refine MoneyAmount into MoneyAmountPreTax and MoneyAmountPostTax. While we’re at it, make sure you can only get to a MoneyAmountPostTax by adding tax to a MoneyAmountPreTax.

public class MoneyAmountPostTax {
    private MoneyAmountPostTax(Decimal amount);
    public MoneyAmountPostTax(MoneyAmountPreTax baseAmount,
                              TaxRate rate);
}

Result

Our initial example then becomes:

public MoneyAmountPostTax OwnProduction(ElectricityAmount annualProduction,
                                        MoneyAmountPostTax electricityPrice)

The typesystem now guards us from many implementation mistakes regarding taxes, that otherwise would have had to be detected in testing. Also the code is much more self documenting.

This was done only by introducing more appropriate types, the structure of the code remained the same. Cleaning up the structure of this module still needed to happen, but it was a lot easier and safer at that point.

It’s also obvious now that this method was named wrong:

public MoneyAmountPostTax YearlySavings(ElectricityAmount annualProduction,
                                        MoneyAmountPostTax electricityPrice)

The talk had a more refined example, but even this light application of types already brought a lot of good.

Conclusion

I can’t really tell you much about domain driven design and I’ve never used F#. But by watching this video, I got a much better understanding of the purpose of the typesystem.

I’ve only touched the surface of domain driven design here. These new types are simple aliases for Decimal, while they could be a lot more restrictive. Not every possible Decimal value is a valid ElectricityAmount for instance.





PS

Making value types in C# takes many keystrokes.

class ElectricityAmount
{
    public ElectricityAmount(Decimal amount)
    {
        _amount = amount;
    }

    public static explicit operator Decimal(ElectricityAmount amount)
    {
        return amount._amount;
    }

    public Boolean Equals(ElectricityAmount other) {
        return this._amount == other._amount;
    }

    public static Boolean operator ==(ElectricityAmount left,
                                      ElectricityAmount right)
    {
        return left.Equals(right);
    }

    public static Boolean operator !=(ElectricityAmount left,
                                      ElectricityAmount right)
    {
        return !left.Equals(right);
    }

    public override int GetHashCode()
    {
        return _amount.GetHashCode();
    }

    private readonly Decimal _amount;
}

But if all you want is an alias, that doesn’t need to be much more painful than it is in F#.

class ElectricityAmount : Alias<ElectricityAmount, Decimal>
{
    public ElectricityAmount(Decimal amount) : base(amount) { }
}

class Alias<TObject, TValue>
{
    protected Alias(TValue value)
    {
        _value = value;
    }

    public static explicit operator TValue(Alias<TObject, TValue> value)
    {
        return value._value;
    }

    public Boolean Equals(Alias<TObject, TValue> other)
    {
        return this._value.Equals(other._value);
    }
    public static Boolean operator ==
        (Alias<TObject, TValue> left, Alias<TObject, TValue> right)
    {
        return left.Equals(right);
    }
    public static Boolean operator !=
        (Alias<TObject, TValue> left, Alias<TObject, TValue> right)
    {
        return !left.Equals(right);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    private readonly TValue _value;
}