Scala seems to have no equivalent of Java's static nested classes

Scala has pervasive support for nesting of classes/traits and objects within larger traits and objects. One long-standing pattern this allows that I find particularly expressive is the “traits as modules” style.

In this style

  • traits define extensible modules that can contain data values, both abstract and concrete methods, and data types (ie traits/classes)
  • Modules are instantiated by declaring an object that inherits one or more modules and ensures all abstract fields are implemented.
  • Modules can extend or depend on other modules by importing their object form, or inheriting from their trait form.

While the popularity among scala programmers of this pattern has waxed and waned over the years, the idea can be traced right back to Scala’s roots. It features in Odersky and Zenger’s 2005 paper “Scalable Module Abstractions” (actually there were a lot of IMO good ideas in this paper that perhaps deserve more attention & appreciation from the modern Scala audience, 18 years later).

However, Scala’s support for “traits as modules” is missing one important capability: it is not possible to natively declare what in Java is termed a static nested class.

Rather, Scala only supports what Java terms “inner classes”, which trail a reference to an instance of the containing trait.

So for example, if one defines a Person module containing a Person case class, every Person instance contains an extra field, a pointer to a PersonModule instance.

trait PersonModule:
   case class Person(id: UUID, name: String)

There are probably times when this is desirable, although I don’t ever use it in practice. There are many times when at worst this is benign and harmless. But unfortunately, in computationally demanding scenarios where there are eg millions of instances of the class in memory, spending 8 bytes per-instance on a completely unused field is a problem.

As a workaround, one can define the class outside the module-trait and export it, which simulates a static nested class:

trait PersonModule:
  export module_internal.{Person}
package module_internal
case class Person(id: UUID, name: String)

However, the workaround causes a number of difficulties. Because the class definitions and companion objects (like Person above) must live outside the module, methods defined in them cannot refer to other symbols defined within the module without hitting circular dependency problems.

It would be much preferable to be able to express static nested classes natively in the Scala language. In my view, static nesting is typically the most useful form of nesting. It allows placing a datatype into a module namespace, without introducing any runtime cost.

1 Like

Scala’s equivalent of static nested classes is class defined in (companion) object:

trait PersonModule:
  import PersonModule.*

  def getPerson(id: UUID): Option[Person]

object PersonModule:
  case class Person(id: UUID, name: String)

All you need to access ‘per-module’ classes is one import per module:

object InMemoryStore extends PersonModule:
  import PersonModule.*

  def getPerson(id: UUID): Option[Person] = None

I think that usually enough and much better than yet another keyword for ‘true’ static inner classes and a lot of subtle differences between trait inner class and object inner class.

2 Likes

The exact equivalent way to do this is to declare the class in the companion object, (Edit: as seigert mentioned).

Java will even consider them as a static inner class when referenced from Java code

2 Likes

Scala also has this “hack” for eliding the reference to the outer class, for when you actually want a real inner class (it kind of sounds like you do) but don’t want the extra field: declare the inner class as final:

class Foo {
  class Inner
}

class Bar {
  final class Inner
}

val foo = new Foo
val bar = new Bar

(??? : Any) match {
  case _: foo.Inner => 0
  case _: bar.Inner => 1 // warning: The outer reference in this type test cannot be checked at run time.
}

Only I’m not 100% sure if this is still the case in Scala 3.

I tried decompiling with javap on Scala 3.3 and observe that, whether or not a nested class is final, it always has a reference to its containing module/trait passed & stored by its constructor.

More broadly, the responses on this thread have helped sharpen my understanding of the tradeoffs.

It’s the module reference that permits nested classes to refer to other fields of their containing trait, regardless of how the module gets stacked with others at runtime. Losing that module ref also means losing the ability to refer to other parts of the module. :+1:

This sounds like something that the Spark folks hit, and worked around with the infamous Closure Cleaner

1 Like

Could it be done in compile time or there are fundamental obstacles?

The outer reference is suppressed if the inner class is private, but not if it is final. Why? Assume

class A:
  class B

Some code in a different compilation unit could have a test like this:

  val a = A()
  val b = a.B()

  def f(x: Any) = x match
    case x: a.B => ???

To implement that test correctly, we need to check whether the outer reference of x equals a.

4 Likes

Experimented further with javap decompilation. I observe that when a nested class is declared private and…

  1. makes no references to fields of its enclosing class, it is generated without an outer reference.

  2. makes any reference to a field/method of the outer class, this transparently triggers the inclusion of an outer ref into its constructor bytecode.

2 Likes

This is just part of the broader difference, that in Scala you can’t inherit static members. Instead static members go in a final companion object.

As you said, in scala nesting rules are consistent and don’t discriminate. So this rule, and difference from Java, applies equally to member classes as to member methods and fields.

2 Likes

Thanks for mentioning this behavior.

I just went on an odyssey from an open ticket through the ecosystem of tickets surrounding the nested final class problem.

I wasn’t aware of the degree of pain it inflicted.

xeno-by invented a workaround that leverages the “package object facade pattern”. He laments, “it’s been plaguing our lives for more than three years now.” That was 2017 and I think he meant his life. But it was on Adriaan’s radar even earlier. Over ten years is a long time to live with a pain point that everyone agrees is just a bug.

There is a Dotty ticket for nested traits without outer pointers. We may call them “withouters” for short. That ticket is tentatively milestoned for Scala 4.

1 Like

There is a Dotty ticket for nested traits without outer pointers. We may call them “withouters” for short. That ticket is tentatively milestoned for Scala 4.

Uh, is Scala 4 a thing? Initially I assumed it was er… a Snytt-ism :stuck_out_tongue_closed_eyes: … but Seth mentioned it as well.

Scala 4 is not a thing, no.

1 Like

It’s not a thing until it’s a Sebisme.