Previous Entry Share Next Entry
Covariance and Multiple Interface Inheritance
anime
he_the_great

(The names used for interfaces, classes, functions may not depict good design; they have been chosen only to make conveying the capability more clear)

dlang
interface AAAAA { aaaaa aaA(); }
interface aaaaa {}

class AaAAaA : AAAAA {
    aaaaa aaA() {
        return aaaaa.init;
    }
}

class aAA : AaAAaA, aaaaa {
}

Excuse me. I think my computer just sneezed.

Interesting thing happened today, I was fighting with C# about type safety. And it seems the Lang.Next talk was released which had some discussion about contra/covariance and why have it.

I prefer static typing. I want the compiler to yell when using something wrong and I lean on that ability heavily. People love REPL from dynamic languages because they get to try things and see how they work, types allow for the that same principle. Making some modifications to a type in terms of structure and behaviors expected the compiler will explain when someone isn't upholding their end of the bargain. And this gives the opportunity to either re-evaluate the design choice being made or to update types to match the changes.

For that reason I want structure my code in a way that the compiler will validate a contract is being fulfilled. This means casts are not an option, such an operation tells the compiler to ignore what it knows and assume the object will fulfill the rule of the type desired. Covariance for function return types was the needed feature to achieve these goals, C# was missing it. I looked into parameterizing my types, but that didn't quit fit into the whole; I've settled on explicit interface implementation, which just adds boiler plate and I don't have mixins to solve that problem.

D, however, does provide such a feature let's dive into it.

dlang
class A {
    A foo() { return A.init; }
}

class B : A {
    override B foo() { return B.init; }
}

The idea here is to provide covariance on return type, and is not valid in C#. Notice that class B has overridden foo() of class A, but it does not adhere to the contract that foo() returns A. Instead it returns B, as we know that class B is_a A it is type safe to make this change. When calling a.foo() where the type of 'a' is known to be class A and the object is of class B, the returned type will be A.

Hopefully the example I've contrived will depict why this is so great. Please don't consider this a design pattern, understand why it works and if coming across a reason this applies then feel free to be frustrated that the language you're coding in doesn't support it.

dlang
interface LoanOffice {
    LoanItem getItemOfLoan();
}
interface LoanItem {}

A LoanOffice is going to provide a method that obtains the item that has a loan on it. I'm thinking financial loan, so LoanItem may just be paperwork about the loan. LoanOffices are really dumb as they only ever give out a single loan, they may do some other useful things like restock staples but lets concentrate on that single loan.

dlang
interface CarDealer : LoanOffice {
    Car getItemOfLoan();
}

interface CamperDealer : LoanOffice {
    Camper getItemOfLoan();
}
interface Car : LoanItem {}
interface Camper : LoanItem {}

Here are some specific types of LoanOffices, each one will use covariance to provide a very specific LoanItem. Basically the same as the A and B class example but using interfaces.

dlang
class RVDealer : CarDealer, CamperDealer {
    interface Truck : Car {}
    class RV : Truck, Camper {}
    override RV getItemOfLoan() {
        return RV.init;
    }
}

Now the first implementation of a CarDealer and a CamperDealer. Someone selling an RV is going to have a LoanOffice which provides a Car LoanItem and it provides a Camper LoanItem since the RV is both of these. I also threw in the Truck interface which for the sake of argument is a Car. The Truck could go further down the path of covariance had there actually been requirements for being a Car.

dlang
void handleCarLoan(Car c) {}
void handleHomeLoan(Camper d) {}

And here is a simplification of why I desired covariance. In this case I'm showing functions which take a specific LoanItem (my actually case was effectively working with the dealers themselves). An RVDealer should be able to provide a LoanItem to these two functions and there should be no reason to cast. An RVDealer already knows that the type it provides fulfills both contracts even if Car and Camper are distinct types.

dlang
void main() {
    auto rvDealer = new RVDealer();
    handleCarLoan(rvDealer.getItemOfLoan());
    handleHomeLoan(rvDealer.getItemOfLoan());

D allows calling of these two functions because it has verified that the types will fulfill all needed contracts. C# would require a cast eliminating the compile-time checks desired. Note: had I instead stored my RVDealer into a CarDealer or LoanOffice variable the compiler would not allow both of these calls to succeed. This is because the type information says that getItemOfLoan() will return a Car and LoanItem respectively but not the Camper.

dlang
    void handleCarLoan(RVDealer.Truck c) {}
    handleCarLoan(rvDealer.getItemOfLoan());
}

And in case you were wondering, the handleCarLoan() can be specialized to our Truck interface and our RVDealer will give us the type to satisfy that requirement.

Conclusion

The ability to use covariance for function return types helps greatly in eliminating type casts and boiler plate. I wouldn't state it has a frequent need but it provides options and logically follows the contracts of the interface.

I'd also like to say that many may notice that all I have is bad things to say about other languages. Well, C# is actually a really good language which I enjoy programming it. The main issues I run into is that it isn't D, the idioms in D have become a part of the way I look to solve a problem. When solving a problem I'm looking for maximizing compiler checks, representing the problem, and eliminating boiler plate/code duplication. And there just isn't any reason for me to write about how great C# is, everyone already knows it.


?

Log in