Expunging `new` from scala 3

To be more explicit: my problem with object extends is that in all other contexts, if you see object you need to remember that the constructor won’t be called at this point but that doesn’t hold here. This risks making the (already somewhat surprising) semantics of object creation even murkier.

1 Like

So here is some real code. I prefer looking at real code to toy examples, where possible.

implied ValueAndThenPosition[Buffer, A] for ParseOneThenOther[Value[Buffer, A], Position[Buffer], Value[Buffer, A]] =
    (lhs, rhs) => (buff, pos) => lhs(buff, pos)(new {
      override def matched(lEnd: NonNegativeInt, value: A) = rhs(buff, lEnd)(new {
        override def matched(rEnd: NonNegativeInt) = ValueResult.wasMatch(rEnd, value)
        override def mismatched = ValueResult.wasMismatch
      })
      override def mismatched = ValueResult.wasMismatch
    })

It’s part of my toy parser combinator library. There are nested call-back instances that handle the parser success and failure conditions. It is, I freely admit, a horrible heap of spaghetti to read. But the point I want to make is that the new keyword is shot through, and it really doesn’t help any, and ideally once the compiler has got at this, there should be no or minimal allocations.

implied ValueAndThenPosition[Buffer, A] for ParseOneThenOther[Value[Buffer, A], Position[Buffer], Value[Buffer, A]] =
    (lhs, rhs) => (buff, pos) => lhs(buff, pos){
      def matched(lEnd: NonNegativeInt, value: A) = rhs(buff, lEnd){
        def matched(rEnd: NonNegativeInt) = ValueResult.wasMatch(rEnd, value)
        def mismatched = ValueResult.wasMismatch
      }
      def mismatched = ValueResult.wasMismatch
    }

This is the same thing with new removed, and with the “block treated as implementation” heuristic. I’ve also stripped out override.

If we were also allowed to in-place (un-)curry method implementations, I could have written:

implied ValueAndThenPosition[Buffer, A] for ParseOneThenOther[Value[Buffer, A], Position[Buffer], Value[Buffer, A]] =
    (lhs, rhs) => (buff, pos) => lhs(buff, pos){
      def matched = (lEnd, value) => rhs(buff, lEnd){
        def matched = (rEnd) => ValueResult.wasMatch(rEnd, value)
        def mismatched = ValueResult.wasMismatch
      }
      def mismatched = ValueResult.wasMismatch
    }

While this is still not ideal, it is, I’d contend, far more readable than the original. We’ve got rid of nearly all the explicit type annotations and (which is a big win for dyslexics like me) reduced the nested brackets and braces substantially.

According to the criteria I have laid out above, it looks like our course of action is greatly restrained, which is a good thing. So, here’s a modest proposal:

  • Objects of any class C can be created by plain applications C(...). This is arguably simpler and more uniform, since it generalizes what we do for case classes already. Case classes have a number of additional properties, which all but one rely on the fact that case classes keep their constructor parameters as fields of a Product. The only extra ability that has nothing to do with this is the construction syntax without having to go through new. It makes sense to decouple that functionality and make it available for all classes, not just for case classes.
  • Facing an application C(...) where C is the companion object of a class we first try an apply method, and only if that fails convert to an instance creation. We have seen this order is essential for backwards compatibility.
  • No other changes are considered at this time. We still keep new both in its simple and in its anonymous class forms. We need some way to express these two functionalities anyway. Furthermore, with the added C(...) syntax being the recommended way of doing things, explicit new will be used only in relatively rare cases (when an anonymous class is needed, or when we have to create an object in an apply method that takes the same parameters). So if we want to change this part of the language, it can well be after Scala 3.0.
7 Likes

It seems good.
If there are ImplicitFunctionClass the scala will be more closer to language of my dreams.

Perhaps I’ve not entirely groked what you are after, but the @functionalInterface annotation on SAMs may be the droid you are looking for.

@FunctionalInterface
trait And[B] {
  def and(lhs: B, rhs: B): B
}

implied BooleanAnd for And[Boolean] = _ && _

Sorry, It seems, I have maken too short description.
I want to say that new is not big boilerplate for me.
It is not very hard problem to make object.apply, for me.
The real boilerplate is the lack of kotlin receiver function

I think it can be made by simple syntactic sugar in most cases. For example in the topic I have maken the example:

abstract class ExtendedFunction{
  def execute():Unit
}
def doSomeThing(f:ExtendedFunction):Unit = {}
def main(args:Array[String]):Unit = {
      doSomeThing{
          println("ok")
      }
     /*it is similar to 
     doSomeThing{ new ExtendedFunction{
        override def execute():Unit = {
          println("ok")
        }
      }    
     */ 
}

How would this be done?

  • Every class will now have a companion object? It’s a breaking change with no obvious automatic fix, and means many more generated classes in the bytecode.

  • Or is this going to be some magic rule that treats a type name as if it was a value under some circumstances, but not other? e.g., A(0) may work but not println(A).

In fact, even the second option is a bad breaking change. Consider:

def A(n: Int) = n + 1
class A(n: Int)
A(0) // currently returns `1`

The thing is either class A or def A could come from another package entirely, via an import. How is the refactoring supposed to work, then?

EDIT: some instances of new would have to be left behind anyway to avoid breaking code; my main qualm with this second alternative really is about the loss of value/type namespace distinction.

2 Likes

This is by far my favorite option. The irregularity of language constructs is familiar but, I think, fundamentally complicates matters. Given that companion objects exist (or that static methods exist, for languages that have those instead), having magical different syntax for default object creation is a peculiar choice. All sorts of factory methods exist for many classes (e.g. Java is replete with .from and .of, and Scala has a bit of that plus lots of .apply); why have an alternate syntactic form for default object creation as opposed to all the other types of object creation?

If it were possible to actually make it act like a method (e.g. you could get a default creation function by doing Foo new _), I think the language would be considerably more regular.

(Same thing with match; rather than being a peculiar language construct, it should be called and act like a slightly-enhanced method invocation.)

The object extends Foo syntax is, admittedly, rather a clunky way to create anonymous functions. For Scala, where this is usually rather rare, I think the benefits are worth it.

A slightly cleaner syntax is available by abandoning extends rather than with. That is,

Foo.new(15) with {
  override def toString = "example!"
}

This syntax could also be extended to relatively transparent forwarders, so you could (given a constructor-like apply)

Foo(12) with {
  override def hashCode = 12
}
2 Likes

Yeah this is exactly what I suggested. Basically let people instantiate classes without new when there’s no ambiguity, leave new in place, start teaching people to use the new-less version unless there’s some real reason they need to disambiguate and then they can use new.

As Martin said, it’s OK for irregularities to remain as long as we’re making the common case slightly smoother, and I’m OK for new to remain indefinitely as something slightly esoteric that most people will not need to worry about day-to-day.

Maybe in future if we decide to get rid of new and replace it with other things we can, but for now this would be 100% backwards compatible (both w.r.t. code and people) and would allow a gradual migration to using the new new-less convention with zero breakage.

3 Likes

X.new will be confusing since it looks like new is a method in object X, but it’s not. It’s a method named this in class X. The naming here is inconsistent. I think to make this make sense we’d need to name the constructor method new and then have constructors all go in companion objects instead of the classes. Or use X.this, which I don’t think is better.

Additionally, since we want to be able to call the constructors from Java, the compiler would have to make the static new methods call the actual constructors, or vice versa.

1 Like

I don’t think there’s an issue with java interop. The dotty compiler can honour any fictions it wishes to, as long as it can biject between those fictions in the scala 3 source and the jvm back-end. Within the jvm, constructors are actually named <init>, not this or after the class name.

1 Like

Why not? At least if object X exists, new should absolutely be synthesized to be in it.

2 Likes

One idiom that I’ve observed in Scala code is to use new to construct objects for which equality is based on object identity, where each new object is fresh, distinct from all others, and to use a new-less syntax to construct objects without identity, intended to be referentially transparent, equal if and only if their components are equal. For example:

new Counter(0) != new Counter(0)

but

Point(1, 1) == Point(1, 1)

When teaching, the concept of equality and identity is often tricky for students, and I’ve found that having a stylistic cue has been helpful.

1 Like

Shouldn’t this be

class A(x: Int) { class B(x: Int) { ... } }

for a nested class? Otherwise A.new(1).B.new(2) doesn’t appear to make much sense.

I assume you mean new (new A(1)).B(2). But that’s only a syntactical limitation which makes sense because of the non-standard associativity. Semantically you can already do this by desugaring to

{ val a = new A(1); new a.B(2) }

This syntax creates an ambiguity between term names and type names. What happens if the same name does not refer to companions? Does it matter if the type name refers to a class with a companion object even though the term name references something else? Or the other way around: The term name refers to an object that has a companion class but the type name refers to some other type?

I think the most consistent interpretation would be to always treat C as a term name. But that raises further questions: What happens when there is no companion object? Do you assume that a class always has one (for consistency)? What if there is a companion object and you decide that C(...) refers to a constructor? Do you still initialize the companion object? (It would be more consistent but bad for performance).

1 Like

@Ichoran
Can you really imagine trying to explain to someone that the “method” (constructor) this in

class C(x: Int) {
  def this() {
     this(0)
  }
}
object C

is actually called with C.new? There is a complete disconnect here between the naming of new and this. The way it is now is familiar to Java people, and new being a keyword makes it clear that constructors are very special and not really methods. If we move to something like X.new then I need a decent story for those two points.

1 Like

I know we need a nod to backwards compatibility, but this is code I have never needed to write in scala. I have genuinely never written a def this constructor, at least not since my first week writing java-in-scala. I’d actually forgotten the capacity even existed.

1 Like

Mostly there with you – I think I’ve done def this fewer than half a dozen times in half a dozen years. I’m pretty sure it’s never necessary, and I haven’t found it terribly helpful in idiomatic Scala…

1 Like

I would instead imagine def new() = new(0). And I would expect the companion would pick it up. this(0) should be the apply method called on oneself, not a constructor call. How confusing!

That actually adds irregularity. Why object ObjectName extends Something is a definition, while object extends Something is an expression? Why can’t I write println(object O extends Something) but println(object extends Something) would be OK?

3 Likes