From the compilation point of view, everything checks out. Base
demands an implementation for foo
by whoever extends it, and this is what Impl
does. Trait A
is allowed to use foo
because it extends Base
. Everything is clear.
However, I see your confusion. I wouldn't really call it circular though; it's more like using something (foo
in A
) before it has been initalized (in Impl
). Reason this works is because you used def
. Compiler knows that this value will be available later (because if it doesn't find it in the rest of the compilation process, it will break) and it just says "I know it will be available at the point when it is invoked (given the compilation is successful), and it's a def
, which means I will be calculating it then and there. So I don't need to do it now".
If you use val
, however, foo
will be initialized at that point in A
so you will get that initial value, which is 0 for Interegers:
trait Base {
val foo: Int
}
trait A extends Base {
val fooA: Int = foo
}
class Impl extends Base with A {
override val foo: Int = 42
}
val a = new Impl
println(a.fooA) // prints 0 although we wanted 42
Note that lazy val
has the same effect as def
(it's also calculated lazy, it's just that it will be calculated only once, at the point of first usage), so modifying the above code to override lazy val foo: Int = 42
would result in printing 42 too.