Proposal to remove early initializers from the language

There’s a Scala 3 proposal to remove early initializers of the form:

class C extends { ... } with SuperClass ...

The rationale is that trait parameters, which are already supported in Dotty, allow you to replace early initializers from your codebase and make the use of traits more consistent with that of classes.

This proposal is open for discussion in the community and will be discussed in our next SIP meeting, where we’ll take all your feedback into account for the approval/dismissal of this feature.

4 Likes

What’s the status in 2.13 ? I think they are already deprecated, but are trait parameters already supported?

The trait parameters SIP linked above will be brought back to life and considered along with the removal of early initializers. To the best of my knowledge, this feature will make it to 2.14, not 2.13.

1 Like

Is it possible to not deprecate early initializer unless trait parameters are implemented?

3 Likes

+1

Having an implemented migration path before breaking peoples code seems like a good idea.

1 Like

I think that was the plan, and that’s what I meant by “considered along with the removal of early initializers”. It doesn’t make sense to remove this without making sure trait parameters will make it :wink:

2 Likes

Maybe I didn’t understand when I read it, but I remember reading somewhere that since 2.14 uses TASTY, it can’t have early initializers.

What TASTY will and will not have is yet to be discussed, so nothing is set in stone. That being said, the most likely path forward is that trait parameters will be first added so that early initializers can be removed.

See https://github.com/scala/scala/pull/6888

  1. IIUC it seems to be planned to later add trait parameters to 2.13 under -Xsource:2.14
  2. Under -Xsource:2.14 the deprecation message is “use trait parameters instead,” otherwise “they will be replaced by trait parameters in 2.14, see the migration guide on avoiding var/val in traits.”
  3. There are other alternatives already. Adriaan posted a trick from Georg Stefan Schmid: new T { val early: Early = e } with U becomes class TParam(val early: Early) extends U; new TParam(e))
1 Like

Another trick is to use a secondary constructor: https://github.com/smarter/twitter-util/commit/4b829be1e6784d666a1579b42fee930e44a49709

1 Like

I actually had no idea such a feature existed.

Are people generally using early initializers together with superclasses? As long as you only need to extend traits, early initializers could be easily supported by desugaring them to anonymous superclasses and using the standard linearization semantics, i.e.

class C extends { ...} with T1 with T2 ...

becomes

abstract class C$early$1 { this: C => ... }
class C extends C$early$1 with T1 with T2 ...

OTOH, they are not an important feature (AFAICT) and the desugaring is so simple that you may as well do it manually where necessary instead of keeping it as a language feature.

What’s the rationale for wanting to remove them? I would hope for a literal { ... } to behave as an anonymous trait and be acceptable anywhere a trait was expected; if trait T = { ... }; new T can be replaced by new { ... } then I would expect the same to hold for new T with S. So it seems to me that removing early initializers makes the language less consistent?

They’re an ad-hoc and intricate solution for the simpler problem of specifying the initialization order whenever traits are involved. Note that the trick by @gsps:

actually solves all the problems early initializers are currently used for, and it’s way more intuitive and consistent with the language. If you don’t want to expose the new parameter to users, you can use @smarter trick instead.

Why isn’t there just one integrated proposal: “replacing early initialisers with trait parameters”?

I think because trait parameters aren’t ready in Scala 2 yet but if they don’t deprecate early initializers in 2.13 they don’t have enough versions to remove them in Dotty, since 2.14 is supposed to be the last version before 3.0.

They are currently more than anonymous traits because they can be used before a superclass, which is not legal for traits:

scala> abstract class A
defined class A

scala> class B extends {} with A
defined class B

scala> trait T
defined trait T

scala> class B extends T with A
<console>:13: error: class A needs to be a trait to be mixed in
       class B extends T with A
                              ^

This complicates the implementation.

After knowing the two ways you can emulate early initializers with constructors, the dependency between trait parameters and early initializers is no longer there. It looks like early initializers could just be removed without conditioning on the acceptance of trait parameters.

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?