Expunging `new` from scala 3

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

That wouldn’t work. It would need to be:

private[this] object _stage extends JFXApp.PrimaryStage { stage => ... }
stage = _stage

Which is quite horrendous, IMHO. Please, just leave new alone!

EDIT: Also note that with Dotty, one can currently write the amazingly simpler:

object SimpleGUIApp extends JFXApp {
  stage = new {
    ...
  }
}

This is because rhs in scene = rhs has expected type JFXApp.PrimaryStage.

It looks like the following is not supported yet, but in principle we could go as far as to also allowing:

    scene = new(width, height) {
      val guiStuff = ...
      contents = guiStuff
    }
2 Likes

@MarkCLewis Looking at your

  stage = { object st extends JFXApp.PrimaryStage ...; sc }

I thought, well, since a singleton in this context doesn’t need a name, you just want the reference, we could make extends return a reference the same way new does when we drop object st. This rids us of a keyword but gains us nothing in semantics. This thread would just become “Expunging extends from scala 3.” It makes some sense, since extends and new both can run a constructor. (Though extends might not.) Maybe extends would be less controversial?

Here’s a more serious idea. What if we allow extends in place of =?

def x extends Array(10)
val x extends Array(10)
lazy val x extends Array(10)

This would let us be rid of new, and would let us have a clear separation between when we’re calling apply vs a constructor. So then

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

becomes

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

It has something butifull.

I do not think it should be an aim.
But I like the idea to consider

  • val
  • object

as something similar.

note:
I am not sure that it will be work without Delayedinit or its alternative

Never mind. My idea doesn’t quite get rid of new, because you would still need it for anonymous constructor calls. I can’t convince myself that extends is better than = new. Maybe it would be good in combination with something else.