Confusion about self-type and override: Why can I successfully override a method without inheriting a base class or trait?

Recently I’m learning Scala’s trait and self-type.

I did some experiments:

trait IA {
  def play()
}

class B {
  this: IA =>
  override def play() = {
    println("B play")
  }
}

What surprised me is this code can be compiled successfully.

But why?

class B just requires the interface trait IA, B itself does not inherit or mixin any super class or traits.

Why I can use the override keyword on the method play?

What is the semantics of the override in this case?

Does it mean that B actually implements the interface IA? (What is the type of the class B?)

Thanks.

2 Likes

I am not expert but I think self type makes sense in another trait, not a class.

I am not sure how your code isn’t just

class B extends IA

Also no need to start trait name with an I :smiley:

Like what would happen if the override def was not there :slight_smile:

If you don’t like class, I can use trait to describe this question:

trait IA {
  def play()
}

trait B {
  this: IA =>
  override def play() = {
    println("B play")
  }
}

This code can be compiled successfully as well.

But you can’t compile the following code:

trait B {
  override def play() = { // <--- method play overrides nothing
    println("B play")
  }
}

Notice that in both examples, B does not inherit super class or mixin traits, but the 1st compiles successfully, but the 2nd one fails.

IMO, this behavior is very surprising.

A self-type requires any implementation using the trait B to also inherit from IA. So there is always a play() method to override when used. Without the self-type, trait B could be used with a class that doesn’t also use IA.
That said, I agree that using a selftype to override a method from that trait would be surprising and I don’t think I’d recommend using it that way.

2 Likes

Can I think of this be a Scala compiler bug?

As you said:

A self-type requires any implementation using the trait B to also inherit from IA.

For example:

trait IA {
  def play()
}

trait IAImpl extends IA {
  override def play(): Unit = {
    println("IAImpl play")
  }
}

trait B {
  this: IA =>
}

class MyClass extends B with IAImpl // MyClass using the trait B to also inherit from IA.

Obviously, this code can compile successfully.

I can even override play in MyClass:

class MyClass extends B with IAImpl {
  override def play(): Unit = {
    println("MyClass play")
  }
}

But if I remove the IAImpl in the definition of MyClass as following:

class MyClass extends B { // <---- illegal inheritance;
  override def play(): Unit = {
    println("MyClass play")
  }
}

Then compilation failed.

In other words, the method play should be from IAImpl, not B.

So IMO, the Scala compiler should not let programmers to override the play in B, because there is no public method play in B or its super class/traits.

Another surprising is that even if I override the play method in B, we still need mix the IAImpl to make compiler comfort.

See following code:

trait IA {
  def play()
}

trait B {
  this: IA =>
  override def play(): Unit = {
    println("B play")
  }
}

class MyClass extends B // <--- illegal inheritance

As you see, it still failed, the overriding play in B has no effect at all.

It is not a compiler-bug.
this: IA means "this type depends on IA", so it is correct to result in compiler-error if trait IA is not mixed into B

Do you mean that there are two semantics of override in Scala ?

  1. If the play is not in the self-type, the semantics of override is static bounded.
    That means the overridden play must be in the super class (or super traits) of currently defining class or trait.

  2. If the play is in the self-type , the semantics of override is dynamic bounded.
    That means the overridden play does not have to be in the super class (or super traits) of currently defining class or trait, but it must be overridden in the eventual mixed class. So we can override it in B in advance.

No, what I mean is that it is illegal to extend B without also extending IA, because B depends on IA (that’s the semantic meaning of this: IA =>), so the definition

class MyClass extends B

is illegal, hence the compiler-error.

It should be:

class MyClass extends B with IA
1 Like

Thanks, I understand what you mean.

But what I mean by saying it may be a compiler bug is not about the illegal expression

class MyClass extends B

I mean the Scala compiler should not allow programmers to override the play in B, unless there are two semantics of override.

Unlike java traits are modules that contain working code. They are not Java interfaces. Scala allows composition. If I say class X extends IA with IB it means that class X can use the methods in the IA and IB traits.

When we use the self type in trait B we are simply saying, this module requires some implementation with those signatures. If it is already available in a trait with which we extend B, then we can instantiate that class. If not we must provide either a class or trait with the required implementation.

Traits are used for composition and not for class extension via overloading. Experiment with the code below to see this. This may be used, for example, for simple dependency injection (earlier versions of MacWire)

trait IA {
  def play(): Unit
}

trait IB {
  def work(): Unit = {
    println("IB work")
  }
}

trait B {
  this: IA with IB =>
  
  def needPlay(): Unit = {
    play()
    println("B play" )
  }
  
  def needWork(): Unit = {
    work()
    println("B work")
  }
}


class MyClass() extends IA with IB {
  override def play(): Unit = {
    println("MyClass play")
  }
  override def work(): Unit = {
    println("MyClass work")
  }
  
}

trait IA1 extends IA {
  def play(): Unit  = {
    println("IA1 play")
  }
}

class HisClass() extends B with IA1 with IB {
  def needPlayAndWork(): Unit = {
    play()
    work()
    println("HisClass" )
  }
}

trait IA2 extends IA {
  def play(): Unit  = {
    println("IA2 play")
  }
}

class SomeClass() extends B with IA2 with IB {
  def usePlayAndWork(): Unit = {
    play()
    work()
    println("SomeClass" )
  }

}

Code here: https://scastie.scala-lang.org/VLRbvNayQwm11nbXcNoXlA

1 Like

Thanks.

But I did not see the use of the override in your code.

Note that what I mean in this topic is about override a method in the trait/class which has a self-type.

Something like:

trait IA {
  def play()
}

trait B {
  this: IA =>
  override def play() = { // <--- Note I override the play() which is in the IA
    println("B play")
  }
}

I did not find such a usecase.

Keep in mind, this is an unusual usage – I can’t recall seeing a usage of override on a self-type like this before.

That said, I think it’s all as-designed, and there’s nothing “dynamic” about it: it’s just a constraint that you are putting on the compiler, that this trait can only be mixed into classes that provide the specified self-type. This means that the compiler can assume that this type exists in the final class, and compile accordingly.

So in your example, you are saying that trait B can only be mixed into classes that also include trait IA, and it will override the meaning of the play() method in those classes. That’s not a common usage, but there’s nothing terribly weird or dynamic about it, and it doesn’t change the meaning of override, because the resolution of the final meaning comes from the compilation of the class, which has all the components to figure out how they fit together.

Remember, traits can’t be instantiated: they’re kind of ephemeral in that sense. What matters at runtime is classes, and you get an error if the compiler can’t prove that the class, as a whole, makes sense.

1 Like

Exactly, it tells the compiler to error out if there’s not play() in IA.
That in it self isn’t very useful here, but it is good practice to use override when implementing methods defined in inherited classes/traits.

The word “dynamic” I mentioned before just means that the overridden method will be determined when the trait is mixed into a final class. It has nothing to do with real runtime dynamic.

Notice that the word “static” and “dynamic” be used by Programming in Scala

The other difference between classes and traits is that whereas in classes, super calls are statically bound, in traits, they are dynamically bound.

Huh – okay, so it is. That’s an unfortunate usage, IMO, given that the word “dynamic” is much more commonly used to contrast with static types in the Scala world: the word’s being overloaded here. I don’t think I’ve ever noticed this particular usage before – I might recommend to Bill that they change it for the next edition.

Anyway: in that sense, sure – super-call resolution is kind of complex when traits are involved, which is why they are using the word “dynamic”, and now I understand your question above. I think that technically overrides are always “dynamic” in this sense if they are defined in traits – I don’t think the self-type changes that.

Keep in mind, the difference between a self-type and a plain old inherited trait is pretty subtle, and in most ways they tend to work the same – enough so that people sometimes question whether it’s worth having self-types in the language. Aside from the difference in how they are declared, and the way that the self-type puts an extra requirement on the inheriting classes (to mix this type in), they’re usually identical, and it’s usually best to think of them that way.

So from the level of trait B in your example, I think the override behaves precisely the same way as it would if B inherited directly from IA, intentionally. (The aspect I’m not quite certain about here is the linearization order of the overrides in the case of self-types. I suspect that it’s exactly the same as it would be if B inherited from IA, but I’ve never tried the experiment.)

2 Likes

Great explanation!

Thanks.

As the book Programming in Scala says:

The other difference between classes and traits is that whereas in classes, super calls are statically bound, in traits, they are dynamically bound.

I think we can add a new judgement, something like:

The another difference between classes and traits is that whereas in classes, override are statically bound, in traits, they are dynamically bound.

That’s true in a sense, but kind of saying the same thing – super calls go up through the override chain. (That’s effectively what super means: call the nearest override before me in the linearization order.) So basically, it’s a different way of thinking about the same fact.

That is the point. Self types are for composition. B will be mixed in with an IA, not extend it.

That’s an interesting mix of edge-cases you found there.

I’m inclined to agree that the override isn’t allowed according to the spec:

If an override modifier is given, there must be at least one overridden member definition or declaration (either concrete or abstract).

but

An abstract member of a class C is any abstract definition M in some class Ci ∈ L(C)

where L(C) is the linearization of C.

The selftype is not part of the linearization of C, and therefore the members of the selftype are not a member of C, so the member shouldn’t be allowed by the spec to have override.

There are a couple of other interesting tidbits here too: your class is not abstract, but still can’t be instantiated. That is surprising to me. I would have expected the class to be required to be abstract, but in this case, the spec is on the side of the implementation.

Also, if play in IA had a concrete implementation, new B with IA fails to compile! The override modifier doesn’t actually make it override anything.

Use of override here should IMO not be allowed. In the grand scheme of things I don’t think it matters much – the compiler doesn’t allow you to do anything unsafe in either case here, nor does it mislead you to think something is safe that is not. You could deprecate it in dotty, and then maybe drop it in 3.1, though there is 2-3 cross-compilation to think of as well. Whether that’s all worth the hassle is questionable.

2 Likes