5 Jul 2016

Multiple Inheritance Comes in Multiple Flavors

C++ supports multiple inheritance, which causes many problems, like the diamond problem.

The C++ approach to multiple inheritance (as with other features in C++) is, "Here are some tools. You can do whatever you want with them, but we haven't thought through whether they actually work well in practice."

Java took a different approach, supporting single implementation inheritance (one superclass) and multiple interface inheritance (a class can implement multiple interfaces). It eliminated the mess of C++, and on the whole works far better. It's an infrequent need to inherit implementations from multiple classes. I would always pick the Java approach to inheritance over the C++ one.

Java 8 took another step forward, by allowing interfaces to have default methods. This lets you evolve an interface by adding a method, without breaking existing implementations. Say the Java committee wants to add a convenience accessor first() to the List interface. They couldn't, in earlier versions of Java. Interfaces were set in stone: you can't, of course, remove a method from an interface, because clients that invoke the method will break. But you can't add a method, either, which should theoretically be a safe operation, since it doesn't break callers. Java 8 lets you do that. You can do:

interface List<T> {
  ...

  default T first() {
    return get(0);
  }
}

This is a default method, In addition to evolving an interface, you can define an interface with functionality in it, like this:

interface JsonSerialisable {
  default String toJson() {
    // Use reflection to serialise all the fields to JSON.
  }
}

Traits 

The name for this is a Trait. Traits can't have instance variables; only method implementations. This gets rid of many problems with multiple inheritance:

First, C++ sometimes ends up with two copies of base class fields in the derived object. Traits have no fields that can be duplicated. In fact, the whole question of whether inheritance is repeated or virtual doesn't arise with traits.

Second, C++ can have a name conflict between fields in unrelated base classes:

class Foo {
 protected:
  int i;
}

class Bar {
 protected:
  int i;
}

class Derived : Foo, Bar {
  void doIt() {
    i = 0;
  }
}

Which i is assigned to? This question goes away with traits.

Third, C++ suffers from a name conflict with methods, too: if a class has two base classes that define the same method, which one is invoked?

Whereas, in Java, if you have a superclass and a trait that both supply a method:

interface Foo {
  default void doIt() {
    System.out.println("The interface is doing it");
  }
}

class Bar {
  public void doIt() {
    System.out.println("The class is doing it");
  }    
}


class HelloWorld extends Bar implements Foo {
   public static void main(String []args){
      HelloWorld h = new HelloWorld();
      h.doIt();
   }
}

This prints The class is doing it. The class wins over the interface, which is anyway a default implementation, to be used if there is no other implementation.

What if two traits conflict?

interface Foo {
  default void doIt() {
      System.out.println("Foo is doing it");
  }
}

interface Bar {
  default void doIt() {
      System.out.println("Bar is doing it");
  }    
}

class HelloWorld implements Foo, Bar {
     public static void main(String []args){
        HelloWorld h = new HelloWorld();
        h.doIt();
     }
}

This generates a compiler error:

HelloWorld.java:13: error: class HelloWorld inherits unrelated defaults for doIt() from types Foo and Bar                                                                                                                                                                                 
class HelloWorld implements Foo, Bar {                                                                                                                                                                                                                                                    
^                                                                                                                                                                                                                                                                                         

This is again better than C++, which compiles the class but rejects attempts to call the method. This violates the is a rule or, formally, the substitution principle: if a Tesla is a Car, then all methods on Car should also work on Tesla.

C++ is even stupider than that — it lets callers disambiguate which version of the method to call! In the above example, you can do:

h.Foo::doIt();

This makes no sense — deciding what a method should do should be the class's responsibility, not the caller's.

Java has none of this complexity and warts.

BTW, notice that the error message complains of unrelated defaults. What if they are the same implementation, inherited multiple ways?

interface Base {
  default void doIt() {
    System.out.println("Base is doing it");
  }  
}

interface Foo extends Base {
}

interface Bar extends Base {
}

class HelloWorld implements Foo, Bar {
     public static void main(String []args){
        HelloWorld h = new HelloWorld();
        h.doIt();
     }
}

This works — there's only one implementation, so there's no ambiguity.

What if one intermediate class overrides the method, and the other doesn't?

interface Base {
  default void doIt() {
    System.out.println("Base is doing it");
  }  
}

interface Foo extends Base {
  default void doIt() {
    System.out.println("Foo is doing it");
  } 
}

interface Bar extends Base {
}

class HelloWorld implements Foo, Bar {
  public static void main(String []args){
    HelloWorld h = new HelloWorld();
    h.doIt();
  }
}

This again works, printing "Foo is doing it".  There's no conflict, since no one disagreed with the Foo implementation.

Another unique aspect of traits is that you can't invoke super from within a trait:

interface Base {
  default void doIt() {
    System.out.println("Base is doing it");
  }  
}

interface Foo extends Base {
  default void doIt() {
    super.doIt();
    System.out.println("Foo is doing it too");
  } 
}

The compiler rejects this

HelloWorld.java:9: error: cannot find symbol                                                                                                                                                                                                                                              
    super.doIt();                                                                                                                                                                                                                                                                         
    ^                                                                                                                                                                                                                                                                                     
  symbol:   variable super                                                                                                                                                                                                                                                                
  location: interface Foo                                                                                                                                                                                                                                                                 

This should logically work — super should refer to Base, but for some reason, Java doesn't permit this.

Those details aside, the main point is that traits let you implement a small amount of functionality in a reusable manner, which can then be used in multiple classes, in a more compositional style of programming. As opposed to an abstract base class, which is an all-or-nothing proposition.

Mixins

One step further than a trait is a mixin. Unlike a trait, a mixin can have fields. For example, you can have a singleton mixin in Ruby. A singleton needs a field to store the single instance.

Mixins can replace the methods of the class, because you start with a class and then "mix in" the mixin, which defines new methods and fields, and replaces conflicting methods.

Implementations of methods in mixins can also call super. The super call gets resolved at runtime. We don't know at compile-time what the superclass is. That makes mixins abstract subclasses.



In summary, there's a continuum of power, from single implementation inheritance (in old versions of Java) to traits (in Java 8) to mixins (in Ruby). All these are better than uncontrolled multiple inheritance in C++. And Java 8 moves one step up the power continuum with traits.

No comments:

Post a Comment