Scala 3: Universal Apply Methods
Let’s take a break from the type system changes and discuss other new features in Scala 3. This post is about universal apply methods, which eliminate the need for using new
when creating instances, most of the time.
For an even more concise summary of most of the notable changes in Scala 3, see my new Scala 3 Highlights page.
You can start reading the rough draft of Programming Scala, Third Edition on the O’Reilly Learning Platform. The first half or so of the chapters are available. I am refining them still, but any feedback is welcome!
One of the many advantages of using Scala 2 case classes is that a companion object is generated by the compiler with an apply
method that delegates to the case class primary constructor (which is usually the only constructor). Hence, for a case class Person
, we create instances using Person(name, age)
without new
. Some other types define object apply
methods for similar reasons, like Seq.apply
, even though Seq
itself is abstract (Seq.apply
returns a List
).
Scala 3 extends the case class scheme to all concrete types, even those in Java and Scala 2-compiled libraries. A synthetic object is generated with an apply
method for each constructor in the class. These methods are called constructor proxies.
Let’s see an example. I’ll use Scala to create a type, but pretend it’s coming from a Java library:
We can create instances with either constructor without using new
.
However, there’s an important difference with how case classes work:
For case classes, the compiler doesn’t generate a proxy for auxiliary constructors. Why the difference?
In Java, it’s more common for types to define and use multiple constructors, than in Scala. It’s especially uncommon for Scala case classes to have auxiliary (or secondary) constructors. (My Person
definition is highly contrived…) There are lots of reasons, like Scala’s support for default arguments, defining apply
methods for special construction purposes in companion objects, etc.
Also, Java doesn’t distinguish between primary and auxiliary constructors, like Scala does. Therefore, it’s necessary for constructor proxies to be generated for all constructors for this feature to be pragmatic.
Here are some rules to keep in mind.
- A constructor proxy won’t be generated if one already exists for that argument list in a user-defined companion object. Similarly, a synthetic object won’t be generated if a companion object already exists.
- When a constructor takes no arguments, rewrite occurrences of
new Foo
asFoo()
. Omitting the parentheses would be ambiguous for the compiler. - When writing a companion object
apply
method, say for some typeFoo
, usenew Foo(...)
to create instances. If you just writeFoo(...)
and the arguments match the parameter list for one of theFoo.apply
methods, then you’ll end up with an infinite recursion! (This rule has always been true, but it’s worth repeating here.) - Anonymous classes require
new
.
The synthetic objects are only generated for concrete types, so if you create an anonymous class while declaring a value, you’ll have to use new:
Universal apply methods eliminate even more occurrences of new
, making Scala code more consistent.
For an even more concise summary of most of the notable changes in Scala 3, see my new Scala 3 Highlights page.
You can start reading the rough draft of Programming Scala, Third Edition on the O’Reilly Learning Platform. The first half or so of the chapters are available. I am refining them still, but any feedback is welcome!