Proposal to remove early initializers from the language

I feel like along the course of this discussion the workaround became the solution but these two paths look like they have differing costs to reach the desired end goal to me.

We now have two currently available alternatives to early initializers and one “coming soon” alternative, trait parameters. In a hypothetical world where all three alternatives were available today, which would be the desired or commonly recommended solution? I ask this because if the desired outcome is trait parameters, then taking a sidestep through secondary constructors complicates the eventual goal.

Consider two scenarios:

  1. We deprecate early initializers when we add trait parameters. I would create a scalafix rewrite to do the migration and don’t foresee much difficulty with that.

  2. We deprecate early initializers before we add trait parameters. I don’t want to leave deprecation warnings in my codebase or turn off fatal warnings, so I need to rewrite this to secondary constructors. Can I create a rewrite to migrate early initializers to secondary constructors? I haven’t considered it thoroughly but my initial feeling is yeah, I could probably pull that off. Now a version later trait parameters are released. If these are a better solution then can I rewrite my secondary constructors to trait parameters, but only the ones that should be trait paramters, not legitimate secondary constructors? Possibly, but this seems far more difficult upon initial consideration.

This is the migration route I would go (I didn’t think of the other two possibilities to early initializers when I first started the thread and answered the first questions). I think early initializers could be deprecated in 2.14 (2.13 is maybe too early).

That being said, I’ll say that yes, indeed, there are several ways to replace early inits. But note that all these ways are fundamentally the same; they are all about using constructors from either classes or traits.

Which one should be used in Scala 2.14/3? Well, my answer to that would be that you pick whether you want to define your logic as a class or trait (you still need to make this choice today), and based on that you either go with old-fashioned constructors in a class or constructors in a trait (trait parameters). Does this make sense?

IIUC the warning was already merged for 2.13.

I still don’t get it. What is the point of trait parameters then? If they are never the best alternative to early initializers, why are they being added at all? If they are the best alternative then if we advocate migration away from early initializers before they are ready almost no users are going to then go back in the following version and migrate a second time to trait parameters, we will just have code that is left in a sub-optimal state. If you are saying that you don’t believe trait parameters are ever the best replacement choice for early initializers then yes, I understand your point, but if you believe that trait parameters are in some cases the best replacement for early initializers then I don’t understand at all why you would prompt the user to replace an early initializer when the most appropriate replacement for it is not yet available.

1 Like

In that case why not deprecate only the use of early initializers with superclasses, and treat {...} blocks unconditionally as anonymous traits? Wouldn’t that avoid implementation complexity while keeping the language more consistent?

I agree there’s a concern with users migrating to a sub-par intermediate solution (adding constructor arguments), but I don’t see why trait parameters would be useless because something that they can replace (early initializers) is deemed to be. Trait parameters have a nice symmetry with class parameters, and thanks to some careful restrictions, they are less tricky to reason about in terms of initialization order.

With the introduction of trait parameters, the only conceptual distinction between a class and a trait is that a class fixes the linearisation order, while a trait does not. (Low-level differences remain due to platform restrictions, of course.)

Our take on early initializers (or anonymous traits, if you will) is that they are a niche feature, which just weighs down the language without much advantage (nor usage, crucially – as it would be too costly to remove commonly used features in terms of upgrade drag/pain).

Unfortunately, early initializers are not specified as anonymous traits. So, re-interpreting them as such wouldn’t simplify the specification nor the implementation, it would just mean starting over. Early vals are merged with the class’s other vals after adding the PRESUPER flag. Ensuring the correct initialization order (and type checking scope) for PRESUPER vals is tricky.

The only viable path I see is either replacing them with trait parameters or keeping them along with trait parameters. Since trait params fully subsume early vals, and are much more elegant, I am strongly in favor of dropping early initializers in favor of trait parameters.

3 Likes

I am in favor of this proposal.

1 Like

I agree, trait parameters are a better solution, and given that I don’t want to warn users about it until they are able to migrate to trait parameters in 2.14. As long as the option to emit warnings for early initializers is only emitted under xsource=2.14 I support them.

On an unrelated note, I also agree early initializers are a niche feature in open source projects whose code I have inspected, but in closed source projects I have had access to I have seen them used quite commonly.

Very much related! This is one of my biggest worries about all of these deprecations: the community build / OSS is not a good model for what goes in private code bases. Who really knows how much these seemingly obscure features are used out there…

1 Like

Possibly corporate projects have a different adoption rate because of the necessarily different incentive structure.

One solution for projects that ignore deprecations would be for the compiler to surreptitiously track compilations (by cwd, independent of the build tool) and if deprecations are ignored long enough, periodically fail a compilation, for example, once every six months. Obviously, a warning would not be sufficient.

[error] This project seems to be getting stale and rank with deprecations. Consider addressing deprecations and upgrading to the latest version of Scala, or if your manager won’t let you, try -deprecation:false to disable this helpful reminder. If you take no action, and you know you won’t, you will see this message again on Jan 31, 2019.

For what it’s worth, the largest Scala project I work on for $ has 1283 files, of which 6 contain early initializers.

That search showed me two cases where someone (not me I swear!) used an early initializer instead of a normal initializer. Like object X extends {} and no with.

Thanks for checking! It just shows every syntactically valid construct arises given enough usage :slight_smile:

1 Like

@adriaanm
I’m in favor of dropping trait parameters, but I have a (possibly dumb) question (I’m not familiar with the nuances of early initializers and trait parameters). Is it not possible to just take an early intialization and rewrite it as the most right-hand-side trait parameter? E.g.,

class A extends {val something = ???} with B with C with D
//changes to
trait $anonSomething (val something = ???)
class A extends B with C with D with $anonSomething

That only works if your something relies in no way to any constructor parameters of A.

Isn’t that always true for early initializers?

Unless I misunderstood something, not quite!

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait A { val early: Int; println(early) }
class B         extends A { override val early: Int = 12 }
class C         extends   { val early: Int = 13 } with A
class D(n: Int) extends   { val early: Int = n  } with A

def newB: B = new B
def newC: C = new C
def newD(n: Int): D = new D(n)

// Exiting paste mode, now interpreting.

defined trait A
defined class B
defined class C
defined class D
newB: B
newC: C
newD: (n: Int)D

scala> newB
0
res4: B = B@1e6060f1

scala> newC
13
res5: C = C@16d431b4

scala> newD(14)
14
res6: D = D@586cc15d

You are mistaken about the strange syntax you forswear.

There is a canonical answer on stack overflow.

And an old email list thread proposing to eliminate or simplify the syntax. That has an interesting discussion about syntax that is not bike shedding.

There was a previous SO answer where the OP called it “naked extends”, and I had called it “optional extends”. The OP asked, Does it extend nothing in particular?

On the email list, I had proposed: class C extends _ { } so if anyone says Scala doesn’t have enough usages of underscore, don’t lay that on me. Now it could mean, extend AnyVal if that works but don’t bug me about some rule about extending AnyVal, extend whatever makes sense.

Yep, or the other way around: an early initializer usually initializes a field that’s abstract in parent traits. If you’d just override it in the template of the class that mixes in those traits, the trait constructors will already have run (and seen the null value) before the constructor gets to the statements/initializers in the class body.

With an example:

scala> trait Compiler { val symbolTable: List[String] }
defined trait Compiler

scala> trait TypeChecker extends Compiler { val someDefinition = symbolTable.head}
defined trait TypeChecker

scala> object Main extends Compiler with TypeChecker {
     |   val symbolTable = List("hi!")
     |   def run = {
     |     println(someDefinition)
     |   }
     | }
defined object Main

scala> Main.run
java.lang.NullPointerException
  at TypeChecker.$init$(<console>:1)
  ... 32 elided
scala> object Main extends {val symbolTable = List("hi!") } with Compiler with TypeChecker {
     |   def run = {
     |     println(someDefinition)
     |   }
     | }
defined object Main

scala> Main.run
hi!

This topic was automatically closed after 30 days. New replies are no longer allowed.