Object-Oriented – Interface vs Generically Constrained Type

genericsobject-orientedtype-systems

In object-oriented languages that support generic type parameters (also known as class templates, and parametric polymorphism, though of course each name carries different connotations), it is often possible to specify a type constraint on the type parameter, such that it be descended from another type. For example, this is the syntax in C#:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

What are the reasons to use actual interface types over types constrained by those interfaces? For example, what are the reasons for making the method signature I2 ExampleMethod(I2 value)?

Best Answer

Using the parametric version gives

  1. More information to the users of the function
  2. Constrains the number of programs you can write (free bug checking)

As a random example, suppose we have a method which calculates the roots of a quadratic equation

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

And then you want it to work on other sorts of number like things besides int. You can write something like

Num solve(Num a, Num b, Num c){
  ...
}

The issue is that this doesn't say what you want it to. It says

Give me any 3 things that are number like (not necessarily in the same way) and I'll give you back some sort of number

We can't do something like int sol = solve(a, b, c) if a, b, and c are ints because we don't know that the method is going to return an int in the end! This leads to some awkward dancing with downcasting and praying if we want to use the solution in a larger expression.

Inside the function, someone might hand us a float, a bigint, and degrees and we'd have to add and multiply them together. We'd like to statically reject this because the operations between these 3 classes is going to be gibberish. Degrees are mod 360 so it won't be the case that a.plus(b) = b.plus(a) and similar hilarities will arise.

If we use the parametric polymorphism with subtyping we can rule all this out because our type actually says what we mean

<T : Num> T solve(T a, T b, T c)

Or in words "If you give me some type which is a number, I can solve equations with those coefficients".

This comes up in a lot of other places as well. Another good source of examples are functions which abstract over some sort of container, ala reverse, sort, map, etc.

Related Topic