Expunging `new` from scala 3

Whether to use extends or : is a separate question. Both work. Java and Scala use extends, C# and Kotlin use :. I believe changing this now for Scala would not gain us nearly enough to justify breaking things.

1 Like

What if I have a dependency on the following Java class?

public class Foo {
  public Foo(String a){
  }
  public static int apply(String b) {
    return b.length();
  }
}
1 Like

The generation of an apply method is suppressed if that method would clash with an apply method already given in that companion object.

Will there still be a way to accomplish the following?

class Uppercase private[Uppercase](s: String)
object Uppercase {
  def apply(s: String): Uppercase = new Uppercase(s.toUpperCase)
}
1 Like

For now, you continue to write new Foo("abc"). Eventually, you need to replace this with

{ object foo extends Foo("abc"); foo }

I should have specified that the synthesized apply technique would apply to Java and Scala classes equally. In this case, no apply can be generated since the user already provided a clashing
apply. It’s an artificially constructed case which will hopefully be rare in practice.

1 Like

Yes:

class Uppercase private[Uppercase](s: String)
object Uppercase {
  def apply(s: String): Uppercase = {
    object result extends Uppercase(s.toUpperCase)
    result 
  }
}

I’m missing a section on performance implications of this change. How much extra bytecode will be generated at call sites and will that negatively impact inlining or other optimizations?

I’m missing a section on performance implications of this change. How much extra bytecode will be generated at call sites and will that negatively impact inlining or other optimizations?

Good point. The performance impliciations would be the following:

  • generated apply methods: These would all be defined inline so would have no runtime footprint at all.
  • replacing new classes with local objects: The only performance-relevant part is that the local object is a lazy val. I.e. instead of
    new C(...)
    
    the compiler sees
    { lazy val x = new C(...); x }
    
    It would be straightforward (and could be mandated) to recognize that the lazy val should be replaced in this case by a strict val, since it is used immediately.

Making body-less object not a subclass will break a lot of code. For example it’s often done to get a unique value for getClass. In fact Lift relies on this excessively.

Also, I still have not heard one single good reason for this change.

Making body-less object not a subclass will break a lot of code. For example it’s often done to get a unique value for getClass. In fact Lift relies on this excessively.

Interesting use pattern. I wonder whether there are other programs than Lift doing this? However, note that can be rewritten by simple regex matching: Add a {} to each object definition missing one.

In scalajs-dom, a common pattern is to define anonymous instance when passing options to an event listener (the current way of using an object with an apply to create the object will soon be deprecated; see https://github.com/scala-js/scala-js-dom/pull/343#pullrequestreview-208454799). The new proposed syntax in the SIP will make such usage verbose (and in typical codebase, we have many such snippets).

Currently, we can do:

elem.addEventListener("foo", { e => ... }, 
                      new EventListenerOptions { capture = true })

With new gone, we will have to do:

elem.addEventListener("foo", { e => ... }, 
                      {object x extends EventListenerOptions { capture = true}; x })

This adds unnecessary verbosity and the introduction of x adds to distraction.

Here is the EventListenerOptions for reference:

trait EventListenerOptions extends js.Object {
  var capture: js.UndefOr[Boolean] = js.undefined
  var once: js.UndefOr[Boolean] = js.undefined
  ...
}
5 Likes

With the synthetic apply, should this not become:

elem.addEventListener("foo", { e => ... }, 
                      EventListenerOptions { capture = true })

Or am I missing something?

EventListenerOptions is a trait and not a class. I don’t think the synthetic apply will be generated for traits.

No, because apply doesn’t subclass it.

Although in this case you’re not overriding, you’re just setting a var, so with .tap you could do EventListenerOptions().tap(_.capture = true). But very often in scala.js you need to extend an anonymous class and override vals.

This new syntax is not workable for it. And once again: There is not one good reason to make this change that anyone has given.

I actually like the proposed expunging of new to enable thinking uniformly about creating (for example) an instance for a class or case class. At the instance creation site, the difference of case vs not-case seems like unnecessary detail, which this proposal addresses.

What I don’t like is how trying to completely avoid new forces obscure and distracting syntax in cases such as EventListenerOptions I mentioned.

3 Likes

While I don’t think this change is necessary in any way (and I’m personally neutral about it), I do think there’s a good deal of clear evidence of demand for it, simply in the number of people who abuse case class solely in order to get new-less instance creation. I come across that pretty often, and have gotten into more than a few arguments about it.

Yes, that’s a want, not a need. But “want” does matter, especially in the aggregate…

4 Likes

I think that this discussion is a good reason. Actually it is not good when the language has 2 ways to do something and when these ways lead to holy wars it is even more bad.

1 Like

I think it is a bad decision. But unfortunately there is not better alternative in scala. So may be, we need better alternative than ‘new’ keyword. For example:

This needs to be split in two proposals:

  1. Generate synthetic apply for all classes to enable new-less syntax 99% of the time. Im wary of generating actual.companions though due to runtime and classloader overhead.

  2. Remove new from the language and replace it with awkward and confusing ‘object x extends’ syntax.

As you might gather, I am pro #1 as long as it is compiler side and had no runtime impact, but wary of #2, which introduces as much complexity as it removes.

Anonymous class creation makes it hard to remove new without awkwardness, and the Array(4) vs new Array(4) example demonstrates only the simplest collision between apply and new - consider what default arguments and varargs do to increase that surface area.

Explaining to beginners that creating new pre sized arrays requires awkward syntax will be a problem.

I suspect im not the only one that would be on board with #1 and scared of #2.

5 Likes

Its not two ways to do something, IMO. Constructors are not normal methods.

Constructors can not return null. Constructors can not return sub types, only the exact type. Constructor bodies have very different rules than method bodies. New lets you create anonymous subclasses.

1 Like

So we have classic holy war between different camps.
Actually I can imagine where ‘new’ can be very important. But I use java in such cases. I use scala in very high bisness logic level. There are a bit different logic in such case.

I don’t remember any null pointer exception from apply methods. So it has theoretical weight in my practice.

It is the main reason why we use apply in the most cases.

There are such use cases where I have needed it. And I think it is just less evil. I would prefer something better.