When migrating a project from Scala 2.13 to Scala 3, I ran into some unexpected issues which I want to share. I don’t know what is a feature and what a bug, so please let me know in case you know.
Scala 3 does not allow to override a val with lazy val:
abstract class A {
val a: Int
}
class B extends A {
override lazy val a = ???
}
results in
error overriding value a in class A of type Int;
lazy value a of type Int may not override a non-lazy value
override lazy val a = ???
The obvious fix is to remove laziness but that’s not always easy: sometimes the value is expensive to compute and only rarely needed, sometimes it is never needed, and hence ??? would be a good enough implementation.
Scala 3 requires to give the types of implicits:
class A {
object B
implicit val b = B
}
results in
type of implicit definition needs to be given explicitly
implicit val b = B
which makes me wonder what happened to type inference?
Scala 3 ignores the result types of overrides:
abstract class A
object B extends A {
val b = 1
}
abstract class T {
val a: A
}
class U extends T {
override val a = B
def foo: Unit = {
println(a.b)
}
}
results in
value b is not a member of A
println(a.b)
which again makes me wonder what happened to type inference.
To be clear, all the above examples are valid Scala 2.13 code.
Not sure about the override-adding-lazy thing, but the fact that the error message is so specific seems to imply it’s an intentional design change. I suggest changing a in A from a val to a def.
@SethTisue, I recently browsed the migration guide but somehow missed the sections you referenced, so thanks a lot for the pointers.
Regarding the lazy val overriding issue, I prefer val over def because it both expresses and enforces the requirement for the method to return the same value on each invocation.
This is where the Scala 3 compiler generates the error message:
cbc4bbd1070 compiler/src/dotty/tools/dotc/typer/RefChecks.scala (Martin Odersky 2019-08-29 21:32:08 +0200 434) else if (member.is(Lazy, butNot = Module) && !other.isRealMethod && !othe
r.is(Lazy) &&
4795fee3460 compiler/src/dotty/tools/dotc/typer/RefChecks.scala (Martin Odersky 2020-08-14 11:04:01 +0200 435) !warnOnMigration(overrideErrorMsg("may not override a non-lazy value"), member.srcPos))
5fd20289318 src/dotty/tools/dotc/typer/RefChecks.scala (Martin Odersky 2016-01-31 16:37:10 +0100 436) overrideError("may not override a non-lazy value")
The error message was introduced by this commit:
commit 5fd2028931874291b3cf1b7efef4fed7119d9316
Author: Martin Odersky <[email protected]>
Date: Sun Jan 31 16:37:10 2016 +0100
Enforce rule that laziness is preserved when overriding.
The fact that the error message can be turned into a warning indicates that there is no technical issue with a lazy val overriding a val.
Now I wonder why this rule has been put into place? Does it matter to the interface user when and how a val is computed? To me, the constness is what matters. @odersky, can you enlighten us?
In my particular case, I decided to replace ??? by a real implementation although it is never used in current production scenarios.
Btw, I think the new rule should be documented in the migration guide.
This was changed to preserve the soundness of the type system. More specifically, we cannot allow lazy vals as prefix of path-dependent types in general (https://github.com/lampepfl/dotty/issues/50), but that means we must ensure that an abstract val cannot be implemented with a lazy val.
Technically, Scala 3 is a different language than Scala 2. While it is borderline miraculous that they were able to find a translation pathway from Scala 2 to Scala 3, that convenience obscures the most important aspect of Scala 3 itself: the CORE VALUE PROPOSITION of 3 is guaranteeing type soundness.
Given Scala 2 had type soundness holes, Scala 3 closes those aggressively to ensure type soundness. It is possible Scala 3 has gone further than is needed to ensure type soundness. In fact, in various discussions in forums, Odersky and other Scala 3 designers have acknowledged this and have suggested some of the stringency might be backed off as the proofs for type soundness become more refined over time.
tl;dr Scala 3 is very aggressive in ensuring and enforcing type soundness.