The Curse of Inheritance

Nov. 4, 2024 Nov. 4, 2024, 7:29 p.m. 217
#programming

The other day I was writing an LP solver. While trying to implement affine and linear expressions, I stumbled upon the following issue: Inheritance weirdly models the real world with them. Let me explain.

Mathematically, any linear expression also is an affine expression: They additionally must map \(0\) to \(0\). Hence, one could define the traits

trait AffineExpression { ... }

and

trait LinearExpression : AffineExpression { ... }

Here, LinearExpression : AffineExpression means that every type that implements LinearExpression must also implement AffineExpression, which by the above reasoning makes totally sense. But how would the traits actually look like? Let's take a closer look:

trait AffineExpression {
    fn getCoefficient(self, var: Variable) -> Scalar;
    fn getOffset(self) -> Scalar;
}

Here, Variable represents a variable symbol and Scalar may be any numeric type like f32 or f64. How does LinearExpression look like? It has nothing to add, really. Instead we are now in the awkward position that the getOffset function must always return \(0\). This may be achieved by simply giving it a default implementation.. but who stops us from overriding it? In Java, the above problem could be solved if the default methods of interfaces were allowed to be final, which (for a good reason) is not the case. Hence, from a data-perspective the above hierarchy does not make sense. One should rather have the roles swapped. That is, AffineExpression extends LinearExpression:

trait LinearExpression {
    fn getCoefficient(self, var: Variable) -> Scalar;
}

trait AffineExpression : LinearExpression {
    fn getOffset(self) -> Scalar;
}

But mathematically, this is completely reversed: Any affine expression that does not map \(0\) to \(0\), can't be linear! Overall, the type system does not let us model the relationship between linear and affine expressions, meaningfully. Of course one could justify the second approach by viewing and structurally decomposing an affine expression as a linear expression plus some offset, which is not too far from the truth. But from this compositional perspective the type constraint LinearExpression on a type \(T\) means that "there is a part of \(T\) that behaves linear" instead of the more natural interpretation that "\(T\) behaves linear". But a trait should express and constrain how a type behaves, rather than reflect how it is composed, the latter being part of its implementation details.