Expunging `new` from scala 3

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.

I withdraw my previous Pre-SIP proposal. It has a fatal flaw that was overlooked by my reasoning with analogy (which is always dangerous!). The flaw is in this part:

The generation of an apply method is suppressed if that method would clash with an apply method already given in that companion object. The meaning of “clash” is the criterion that currently determines whether an apply method for a case class should be generated.

This works fine for case classes, but breaks down for normal classes. Why? Consider an idealized Array class:

class Array[T](length: Int)
object Array {
  def apply[T](xs: T*): Array[T] = ...
}

Under my previous proposal, Array(10) would again produce an uninitialized array of length 10 instead of an array of length one with element 10. So this would change the semantics of existing
programs.

The same rule works OK for case classes because we have always generated an apply method for them.

I’ll post in the next comment a new pre-SIP proposal that implements function call syntax for instance creations in a different way. I leave aside for the moment the part dealing with how to replace new expressions

1 Like

Pre SIP: Creator Applications

Creator applications allow to use simple function call syntax to create instances
of a class, even if there is no apply method implemented. Example:

class StringBuilder(s: String) {
   def this() = this(s)
}

StringBuilder("abc")  // same as new StringBuilder("abc")
StringBuilder()       // same as new StringBuilder()

Creator applications generalize a functionality provided so far only for case classes, but the mechanism how this is achieved is slightly different. Instead of an auto-generated apply method, we add a new possible interpretation to a function call f(args). The previous rules are:

Given a function call f(args),

  • if f is a method applicable to args, typecheck f(args) unchanged,
  • otherwise, if f has an apply method applicable to args as a member, continue with f.apply(args),
  • otherwise, if f is of the form p.m and there is an implicit conversion c applicable to p so that c(p).m is applicable to args, continue with c(p).m(args)

The proposal is to add a fourth rule following these rules:

  • otherwise, if f can be interpreted as a type designator that designates a class C with a constructor that is applicable to args, continue with new f(args).

Motivation

Leaving out new hides an implementation detail and makes code more pleasant to read. Even though it requires a new rule, it will likely increase the perceived regularity of the language, since case classes already provide function call creation syntax (and are often defined for this reason alone).

7 Likes

What does “converted” mean here ?

otherwise, if f can be converted to a type name

I just fixed it.

So if I understand correctly, we’d have the following semantics?

val A = (x: Int) => x + 1
class A(x: Int)

Some(A(123))     // has type Some[A]
Some(123).map(A) // has type Some[Int]

It’s not a showstopper, but can be surprising.

I’m all for this proposal as long as it does not include deprecating new, which is very useful for DSLs (even more so in Dotty, where one can have the type being new'd be inferred from the expected type!).

Also, couldn’t the scheme be unified with the way Java-static members work? i.e.,

  • all classes are assumed to have a fictive Java-static apply constructor method; and

  • if A.foo (where foo may be apply) does not refer to a member in the companion object, then we look for Java-static members of A named foo to select.

I agree.

2 Likes

This matches the current implementation but neither “matching” of methods nor overload resolution in the spec mention repeated parameters at all. This works for matching (meaning they don’t match) but as far as I can tell there is no rule which explains why apply(Int) takes precedence over apply(Int*).

But in the end both interpretations cause migration problems. If Array(10) calls the constructor, you’re silently breaking code, which is very bad. If Array(10) still calls the old apply method, any automatic migration tool must not change new Array(10) to Array(10) (and you need to keep new around as a fallback or provide a different rewrite that is not too ugly for common cases like new Array).

1 Like

I’ve always found this part of Array confusing, and prone to errors in any case. I would prefer all uninitialised arrays with specified dimensions to go through a well-named method on the companion. So for example, require the use of Array.ofDim for this.

Is this a source-rewrite tooling fixable issue? To mechanically rewrite new Array[Int](len) to Array.ofDim[Int](len)? Or does this happen for enough non-array cases that it’s a realworld problem?

If Array(10) still calls the old apply method, any automatic migration tool must not change new Array(10) to Array(10) (and you need to keep new around as a fallback or provide a different rewrite that is not too ugly for common cases like new Array ).

In fact a rewriting tool could be smarter than that. It could try to replace new Array(10) by Array(10) and observe whether that resolves to the same symbol. If it does, it can go ahead.

About keeping new around: yes, probably. That part of the proposal has been dropped for now.

From the educational standpoint, I want to throw in a little support for this. Having the ability to create new instances without using new more uniformly will definitely help simplify the language for the novice programmer. So part 1 is a big plus in my classroom.

On the other hand, the proposed syntax for anonymous classes scares me a bit, only because the inline form requires two statements in a block. I would need to think more about how often we use this. I think the main place it happens is when making calls on Java libraries or thin Scala wrappers. The primary one that occurs to me is ScalaFX when we do GUIs and graphics. The standard way I have students write that code now is using nested anonymous classes. It looks something like the following.

object SimpleGUIApp extends JFXApp {
  stage = new JFXApp.PrimaryStage {
    scene = new Scene(width, height) {
      val guiStuff = ...
      contents = guiStuff
    }
  }
}

I’m trying to imagine what this looks like with the modified style, and I don’t think it looks good. Perhaps something like this.

object SimpleGUIApp extends JFXApp {
  stage = { object st extends JFXApp.PrimaryStage {
    scene = { object sc extends Scene(width, height) {
      val guiStuff = ...
      contents = guiStuff
    }; sc }
  }; st }
}

There is also a non-nested style, and I might just have to switch to that. However, for things like event listeners I agree with @ramnivas that this is going to make for much uglier code.

JFXApp uses delayedInit which is going to be dropped.
So I think more precise example can be:

object SimpleGUIApp extends JFXApp {
  object stage extends JFXApp.PrimaryStage { stage => 
     override def body():Unit = {
        ....
     }
  }
}

So object name extends is not big overhead when we compare it with other code.

1 Like