Proposal: Creator applications (going new-less)

This approach was tried and rejected:
http://dotty.epfl.ch/docs/reference/other-new-features/creator-applications.html#discussion

1 Like

My thought was that generating a companion for every class would bloat the bytecode among other drawbacks.

Instead, a (jvm) static apply method could always be generated for each constructor, that delegates to it directly. The additional bytecode size for this is much smaller than a companion. Companion apply methods that return the class type would have their implementations in static methods on the class, which the companion delegates to.
So a library compiled to call Animal("slug") on a plain class would be wired up to the static factory method, and adding an apply method on a companion later would then modify the contents of this static method.
This would mean the binary compat vs source compat story is a bit better - add a companion apply method, and code compiled previously would pick up the implementation without a recompile.

There are some issues that make this non-trivial – jvm static method signatures can collide with instance methods, etc.

But it would help with the java interop story in general if more companion members were actually static elements on the class (with forwarders from the companion object instance). It would make it a lot easier to write wrapper-apis that allow java or other jvm languages to call Scala libraries, for example, or better, to share more of the api between a scala-idiomatic api and one that is meant for other jvm languages to use.

There are drawbacks to any approach that ‘fixes’ this issue as well. Users might be surprised when suddenly new Animal("ant") is not the same as Animal("ant") when it was in previous versions of the library, so I fall back to most of my prior arguments – adding or modifying apply methods in your api contract has consequences and there is no free lunch. Either the behavior changes at link time or compile time, but there IS a change, and the library writer needs to be aware of what it is and communicate it to users.

In that sense, the current proposal is the simplest solution possible if one desires plain classes to not require ‘new’ to be used in the source code. All solutions have drawbacks, even the status-quo. There is wisdom in choosing the simple solution that doesn’t involve modifying the compiler back-end, and so I feel that this proposal has more positives than negatives.

1 Like

Static methods are inherited.

Why are these issues prohibitive for regular classes but not for case classes?

Right now it’s the other way around. A static method on the class forwards to the instance method of the companion object. I think it’s important that the implementation is in the instance method because objects in Scala can inherit from classes and traits.

I did not draw these conclusions, so I can only guess. What I see: using the generate-an-apply-function approach can result in an ambiguity problem. This is already the case for case classes and I need to work around it from time to time. The question here is if we want to add ambiguity problems to code bases which are ambiguity free today in scala 2 when they upgrade/switch to dotty – this could make migrations much harder. Even worse, in some cases the behaviour could change as the generated apply method is more specific than the custom one. So potentially adding bugs when the code is compiled with dotty without noticing it.

IMO it would still be a feasible approach if we

  • do not generate an apply in case of ambiguity and emit a warning in 3.0 and turn it into an error in 3.1 (i.e. generate the apply in all cases)
  • emit a warning if the apply method is more specific (also for case classes)

However, I see additional problems with that approach:

  • we generate companion objects for all classes even though we don’t need it
  • sometimes I want to hide the constructor and provide my own apply method (factory method) and not provide the auto-generated one. For case classes there is no way to do this as far as I know, the public apply is always there. That’s kind of OK for case classes and for consistency reasons the same should apply to classes (if this approach is taken).

IMO the current approach where omitting new is really the same as using new is good. but I would like to see an easy way to delegate to the constructor from an apply (maybe something for 3.1 :wink: or did I miss something and it’s already there?).

In that case, it may make sense to also drop the generated apply method for case classes.

1 Like

Indeed :+1: , would:

  • remove the ambiguity issues
  • allow to have a custom apply which has a higher precedence
  • allow to have a private constructor and a non-public apply

However, then we probably want an easy way to delegate to the constructor already in 3.0

It would indeed be nice if we could do this. Right now the problem is that companions of case classes implicitly extend function types which are implemented by the apply method. We’d have to also drop this
concept from the language. Maybe not a bad idea, except that it would cause further migration hassles,
and we already have enough of those…

4 Likes

A bit of digging lands me 10 years ago, where you explained that that was added for backwards compatability at https://stackoverflow.com/a/3054165/381801

Thinking about the use case for that, is there eta expansion for constructors? On the one hand, I’d think so. On the other, Foo in call(Foo) becomes ambiguous in whether it means passing the term Foo, or passing the eta expanded constructor of the class Foo.

In current dotty, it’s the following:

class Creator(){}
def callit[A](f: () => A) = f()
callit(Creator _) //no: "Only function types can be followed by _ but the current expression has type"
callit(Creator) //no: "Not found: Creator"
callit(Creator(_)) //no: "Wrong number of parameters, expected: 0"
callit(() => Creator()) //yes

Should any of those no’s work? new makes it more obvious that you’re doing something other than just calling a method which makes it less surprising that the above no’s are no’s.

1 Like

This topic has now been open for over 30 days. If anyone wants to add anything further or make any kind of closing or summary statement for the committee, please do so this week, before we close the topic.

1 Like

I’m tentatively opposed, as I see the arguments of both sides as equal and negating one another, making the change a neutral net worth.

IMO, @jducoeur’s and @tarsa’s argument is the strongest argument in favor of the change – preventing developers from abusing case classes just because they provider new-less constructors.

On the other hand, I find @nafg’s and @morgen-peschke’s opposing arguments important as well – signal, not noise; blurring the distinction between regular and case classes; real work in constructors.

@Ichoran I am very much in the opinion that the new keyword has a purpose of reminding people of things they do need to be reminded about. Well, perhaps need is a strong word in this case, but I do think the keyword is not without significance.

new tells me that I am constructing a non-value class, that has non-trivial methods that my current unit is dependent on. In many cases, I would like to be able to mock this new class when testing my unit, and I can do this only if I inject said new class into my unit (via constructor or method argument).

This is more eloquently described by Miško Hevery (already referenced in this thread) in his general post on Writing Testable Code and the more specific one about How to Think About the “new” Operator with Respect to Unit Testing.


If I may, I’d like to add another idea into the pile. How about an additional keyword / annotation that will make the compiler automatically generate an apply method?

@Apply
class MyLovelyFoo(bar: Bar)
// or
apply class MyLovelyFoo(bar: Bar)
// or
class apply MyLovelyFoo(bar: Bar)

This could help prevent the abuse of case classes for the sake of new-less constructors for those who do not care for the distinction between the types of classes or the significance of the keyword, while still allowing class authors who care about these things keep the distinction and the explicit constructors.

2 Likes

It does not work for us, because we broadly use factories, so I can see more disadvantages with ‘new’.

I wonder whether it would be better to use abstract classes to prevent “apply generation”

2 Likes

Perhaps with effect tracking of references we can request that a value can only be assigned with a new reference, which would also help in overload resolution of constructors vs apply:

class Foo()
object Foo:
  // here fresh implies that only a fresh value can be returned,
  // so this is not recursive, however fresh variables can not be assigned to this result
  fresh def apply(): Foo = Foo()

def cache(fresh x: Foo): Unit = ???

cache(Foo()) // calls constructor unambiguously

Here is a nice puzzler.

class Foo(block : => Unit = {println("A")}) {
  println("B")
}

val justFoo = Foo {
  println("C")
}

val newFoo = new Foo {
  println("C")
}

What do you think? Does justFoo and newFoo print the same thing?
If you find yourself straining your brain, we have a problem.

Here is a scastie of what really happens: https://scastie.scala-lang.org/WcsEtJ3GT9mHOGqCvwAR8g

1 Like

Good point.

For clarity and consistency, the second one should be new _ extends Foo { ... }. Then there’s no surprise.

(This is orthogonal to the creator applications issue; apply methods on the companion class create exactly the same illusion in 2.x.)

1 Like

Yes, but that is an illusion created on purpose (well, usually). Here the compiler itself forces this illusion. So without any apply methods, there shouldn’t be any difference for a given class Foo between new Foo and Foo, but there is.

There shouldn’t be any difference either way. How confusing!

If you want to create an anonymous class, you should mean it. Having to rely on subtle cues like “oh, no parameter block” or something isn’t a great way to do it. It should just be clearly very different, like (as I suggested)

val thingy = new _ extends C { ... }
1 Like

This issue wouldn’t exist if some inconsistencies with classes and constructors were fixed.

  1. For some reason, constructors always have at least one parameter list. If none is provided one is added implicitly, both at the definition and use site. If they behaved more like methods the compiler would say something like “missing argument list for constructor Foo”. You would have to write new Foo() { ... } instead.
  2. Parameters to regular methods can be provided with {} instead of (). With a constructor suddenly you’re defining an anonymous subclass instead.
2 Likes

I agree we should have a clearer syntax for anonymous classes, but we want to get rid of new.

How about creating an anonymous class like this:


val a = (extends A){ ... }