A proposal for multiple class inheritance that resolves the Diamond Problem

I know this is controversial, please don’t kill me :wink: I think multiple class inheritance would be useful in many circumstances, and a good tool in the toolbox for some specific problem structures.

The issue typically described by opponents to this idea is what is called the Diamond Problem of how to resolve ambiguity. But I think there is a way to accomplish a good design for the problem.

Let’s say you have this basic structure:


abstract class Animal{
    abstract fun move()
}

abstract class Dog: Animal(){
    override fun move(){
        // run
    }
}

abstract class Snake: Animal(){
    override fun move(){
        // slither
    }
}

But you want to invent a new breed of Animal called SnakeDog. The issue now, is that they both point towards Animal through inheritance, so how would a SnakeDog do move(). Well, why not like this:

class SnakeDog: Snake(), Dog(){
    resolve Animal through object: Animal(){
        override fun move(){
            // slither-run in some way
        }
    }
}

The resolve keyword would clear up any use of a SnakeDog’s movement. And if you reference the SnakeDog through one of its subclasses then the move function would still be resolved (just like if you’ve overridden it with singular inheritance).

It would be possible to call Dog.super.move() and Snake.super.move() from inside the resolving class, in case you want to take advantage of the super class overrides.

Maybe you disagree about the keywords or maybe about the whole premise of this being useful in any way, but it would allow us to tackle ideas with more composition.

I hope it is possible to consider the proposal :slight_smile:

Actually there is nothing Kotlin could do for a real multiple class inheritance
since it is bound to JVM specifications

But, you can use interfaces for multiple-inheritance support.

The downsides of interfaces are:

1 - they are not classes, so you can’t inherit multiple existing classes.
2 - they can’t have actual backing fields

For the first issue, there is nothing we can do about.

But for the second issue, there is actually two solutions:

1 - traits from groovy
2 - some global weak map to back the fields

2 Likes

I like the resolve syntax, since it is very readable. But I can not say, whether there is a way around the single-inheritance limitation of the JVM. At least Scala has found a way around with traits used as mixins.

1 Like

@LSafer already told why this is not possible in Kotlin.

But on a hypothetical level (if it would be possible to add bi-inheritance of classes to Kotlin) regarding your proposal:

  1. What happens with methods that are in both superclasses (Snake and Dog), but not in Animal?
  2. What happens when there are two interfaces with method move (e.g. Animal and Movable) of which both Snake and Dog are inheriting?
  3. Why creating an additional delegation object for resolution?

Why making it so verbose, why not using the solution Kotlin (and Java since Java 8) already uses for the Diamond Problem with conflicting default implementations from interfaces: making it obligatory to override the method in any bi-inheriting class?

class SnakeDog : Snake(), Dog() {}
// -> compiler error: "SnakeDog has to explicitly override method 'move' 
//    since it is defined in both superclasses: Snake and Dog."

// compiling example
class SnakeDog : Snake(), Dog() {
    override fun move() {
        Dog.super.move()
        println("zzzzz")
    }
}

Since Java 8 (and therefore in Kotlin), the Diamond Problem is solved for simple method invocation.
So, in both, your suggestion but also the simpler syntax in this post, the open question remains how this object actually gets created. Calling the constructor normally creates an object - here the SnakeDog constructor seems to call constructors of both superclasses (Snake and Dog), which normally would create two objects. Unlike the three questions above, this last question is solvable, but it is has to be tackled since it is not-trivial.§

§ E.g. let’s say to create a SnakeDog the Snake constructor is called first and the Dog constructor is called second.
But what happens if the Snake constructor call already calls the move method at the end, but the overwritten move method uses Dog.super.move which probably uses Dog attributes which are not yet initialized since the Dog constructor call has not yet happened?