Trait mixin with conflicting members: Why my code can be compiled successfully?

Viewed 340

I'm sorry, this question may be a bit long because I want to describe the problem and my understanding as much as precisely.

Recently I'm learning Scala's trait system.

I did some experiments about conflicting members, see the following code:

trait TA {
  def play() = println("TA play")
}

trait TB {
  def play() = println("TB play")
}

trait TC {
  def play() = println("TC play")
}

class MyClass extends TA with TB with TC {
}

Of course, this code compile failed as expected:

Error:(13, 8) class MyClass inherits conflicting members:
  method play in trait TB of type ()Unit  and
  method play in trait TC of type ()Unit
(Note: this can be resolved by declaring an override in class MyClass.)
class MyClass extends TA with TB with TC {
      ^

My understanding is that

The linearization of class MyClass is {MyClass, TC, TB, TA}, but since there is no override on the play of TB and the play of TC, so it compiled failed.

One workaround is mark override on TA, TB, TC, as following:

trait IPlay {
  def play()
}

trait TA extends IPlay {
  override def play() = println("TA play")
}

trait TB extends IPlay {
  override def play() = println("TB play")
}

trait TC extends IPlay {
  override def play() = println("TC play")
}

class MyClass extends TA with TB with TC {
}

OK, this code can be compiled successfully as expected.

But what about this:

trait TA {
  def play() = println("TA play")
}

trait TB {
  def play() = println("TB play")
}

trait TC {
  def play() = println("TC play")
}

class MyClass extends TA with TB with TC {
  override def play(): Unit = {
    println("MyClass play")
    super.play()
    super[TC].play()
    super[TA].play()
    super[TB].play()
  }
}

(new MyClass).play()

// -- Output:
// MyClass play
//   TC play
//   TC play
//   TA play
//   TB play

What surprised me is that this code can also be compiled successfully.

The linearization of class MyClass still be {MyClass, TC, TB, TA} and there is no override on the play of TB and the play of TC, the only difference is that I added an override on play in MyClass.

Why it can be compiled successfully?

Note that Scala is not Java or C#, there is no default behavior of conflicting members.

In Java, if you don't mark a method as @Override, the default behavior is overriding.

In C#, if you don't mark a method as override, the default behavior is hiding.

In Scala, if you don't mark a method as override, there is no default behavior, they should be conflicting members, IMO.

Therefore, I think the above code compilation should be failed, but why it can be successful?

Or my understanding is wrong.

very thanks.

2 Answers

Too see where the potential conflicts come from, we need to adopt the "point of view" of MyClass.

Let's review your examples to see why you get these results (to simplify I'll use 2 traits instead of 3).

First example: the usual conflict

trait TA { def play() = println("TA") }
trait TB { def play() = println("TB") }
class MyClass extends TA with TB {}

This fails because both traits define a play method with the same signature, but it's not clear which one to choose for MyClass.play. One could argue that "the compiler could choose TB.play because it's the last mixed-in trait", but since no intent of overriding something has been declared, Scala rejects it. It could work in principle, but the Scala team chose to force you to express your intentions more clearly.

How to do that?

Clear intentions 1: override in trait

trait T { def play(): Unit }
trait TA extends T { override def play() = println("TA") }
trait TB extends T { override def play() = println("TB") }
class MyClass extends TA with TB {}

You 2nd example shows one way of expressing the intention of overriding. Since TA.play and TB.play are declared with override, it's clear that they intend to override the method play(): Unit that is in scope. That's why the compiler allows TB.play to "replace" TA.play in MyClass.

Clear intentions 2: override in class

class MyClass extends TA with TB {
  override def play(): Unit = {
    super.play() // super[TB].play()
    super[TA].play()
  }
}

Overriding the method in the class is another way of solving the problem. Here we know exactly what MyClass.play is, since it's explicitly defined. From the point of view of MyClass there is no ambiguity.

The compiler is fine with super[TA].play() because it's clear which method is called: the one defined in trait TA by def play() = println("TA"). So super[TA].play() will execute println("TA").

super.play() is a bit less explicit, but the rules of class composition are clear: super in this context refers to the last mixin, i.e. it's the same as super[TB].

You have defined behavior in the subclass manually. Why are you surprised? What is incorrect in this case?

I think the main confusing thing in the example is that “super.play()” is legal and relates to the last inherited member - TC. I think this is just a workaround when we just take the last. I suppose they could resolve the first case (that doesn’t compile) by this workaround - just take last inherited implementation.

Related