dzy
dzy

Reputation: 171

Overriding attribute from child class erases it from trait in Scala

If a class inherits from a trait, and they both share a parent, it seems as if overriding an attribute of the parent will make it inaccessible from the trait. Namely

class TestClass(arg: String) extends Parent(arg) with TestTrait {
  override val foo = "hey"
}

trait TestTrait extends Parent {
  val bar = foo
}

abstract class Parent(arg: String) {
  val foo = arg
}

Then running

val c = new TestClass("hello")
c.bar

will return null. This seems like counterintuitive behavior for me. Could anyone explain what the general inheritance rules and rationale behind them are to me?

EDIT: Thanks for the responses. However, I'm still confused as to why this works then:

class TestClass(arg: String) extends Parent(arg) with TestTrait {
  // override val foo = "hey"
}

trait TestTrait extends Parent {
  val bar = foo
}

abstract class Parent(arg: String) {
  val foo = arg
}

Running the same as before will successfully produce "hello". Based on the explanations provided, I would've have expected null again. Sorry for not provided this context in the original phrasing.

Upvotes: 1

Views: 909

Answers (2)

mikołak
mikołak

Reputation: 9705

Normally, val s are evaluated in sequence, meaning both in-type declaration sequence, and inheritance declaration sequence.

If there would be no override, bar would be evaluated based on the definition from the Parent constructor, since it is executed before the TestTrait constructor (because TestTrait is a subtype of Parent). So, bar would have whatever value it has in Parent.


However, since you override foo in TestClass, the evaluation takes place only once TestClass's constructor gets invoked, and this is after TestTrait.

The best rationale I could find for this in the SLS is within 5.3.1:

The signature and the self constructor invocation of a constructor definition are type-checked and evaluated in the scope which is in effect at the point of the enclosing class definition, augmented by any type parameters of the enclosing class and by any early definitions of the enclosing template. The rest of the constructor expression is type-checked and evaluated as a function body in the current class.

(emph. mine)

Implying the following pseudocode:

class TestClass(arg: String) extends Parent(arg) with TestTrait {
   Parent(arg) //arg is ignored, since TestClass overrides foo. 
   TestTrait() //foo is still null at this point
   override val foo = "hey"
}

Essentially boiling down to the same case as:

class A {
   val a: String = b //a will be null, since b is not yet evaled
   val b = "A"
}

(only without the luxury of a compiler warning)


In hindsight, this quirk is in fact hinted by Scala's language feature of early definitions. With your code, its use would look like the following:

class TestClass(arg: String) extends {
  override val foo = "hey"
} with Parent(arg) with TestTrait

trait TestTrait extends Parent {
  val bar = foo
}

abstract class Parent(arg: String) {
  val foo = arg
}

scala> new TestClass("hello").bar
res0: String = hey

Upvotes: 1

dcastro
dcastro

Reputation: 68660

That's because constructors are executed top to bottom (from less derived type to most derived type).

Which means TestTrait is constructed before TestClass -> which means foo is null while TestTrait is being constructed.

Parent (arg = null, foo = null)
          \
           \
    TestTrait (bar = null)           Parent(arg = "hello", foo = "hello")
              \                      /
               \                    /
             TestClass(arg = "hello", foo = "hey")

This may seem surprising, because dynamic dispatch works the other way around - methods are called on the most derived type first, and then on less derived types (if super.method() is invoked).

But there's a good reason for this: the initialization of a type often depends on the correct initialization of its base type.

Upvotes: 1

Related Questions