Scala 3: Universal Apply Methods

Dean Wampler
Scala 3
Published in
3 min readJan 25, 2021

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.

After a Chicago Snow, © 2021, Dean Wampler

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:

No “new” required, even for a “Java” type.

We can create instances with either constructor without using new.

However, there’s an important difference with how case classes work:

Case class auxiliary constructors still require “new”.

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 as Foo(). Omitting the parentheses would be ambiguous for the compiler.
  • When writing a companion object apply method, say for some type Foo, use new Foo(...) to create instances. If you just write Foo(...) and the arguments match the parameter list for one of the Foo.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!

--

--

Dean Wampler
Scala 3

The person who is wrong on the Internet. ML/AI and FP enthusiast. Lurks at the AI Alliance and IBM Research. Speaker, author, pretend photographer.