scalavalue-classuniversal-trait

Value classes, universal traits and the necessity of instantiation


In the specification of value classes, it says:

A value class can only extend universal traits and cannot be extended itself. A universal trait is a trait that extends Any, only has defs as members, and does no initialization. Universal traits allow basic inheritance of methods for value classes, but they incur the overhead of allocation. For example

trait Printable extends Any {
  def print(): Unit = println(this)
}
class Wrapper(val underlying: Int) extends AnyVal with Printable

val w = new Wrapper(3)
w.print() // actually requires instantiating a Wrapper instance

First Question

Now, I would take this to mean that the following (probably) does not require instantiation:

trait Marker extends Any
class Wrapper(val underlying: Int) extends AnyVal with Marker {
  def print(): Unit = println(this) //unrelated to Marker
}

val w = new Wrapper(3)
w.print() //probably no instantiation as print is unrelated to Marker

Am I correct?

Second Question

And I would think there is an even chance as to whether this requires instantiation or not:

trait Printable extends Any {
  def print(): Unit //no implementation
}
class Wrapper(val underlying: Int) extends AnyVal with Printable {
  override def print() = println(this) //moved impl to value class
}

val w = new Wrapper(3)
w.print() // possibly requires instantiation

On the balance of probability, I would also think that no instantiation would be needed - am I correct?

Edit

I'd not thought about the exact implementation of print() in the example:

def print(): Unit = println(this)

Let's say that I used the following instead:

def print(): Unit = println(underlying)

Would these cause instantiations?


Solution

  • Am I correct?

    No, we can see it if we emit the final compilation output with -Xprint:jvm:

    <synthetic> object F$Wrapper extends Object {
      final def print$extension($this: Int): Unit = 
        scala.Predef.println(new com.testing.F$Wrapper($this));
    

    This is due to the fact println has a type signature requiring Any, so we're shooting ourselves in the foot here since we're effectively "treating the value class ttpe as another type".

    Although the call is dispatched to the static method call:

    val w: Int = 3;
    F$Wrapper.print$extension(w)
    

    We're still incurring the allocation inside print$extension.


    If we stray away from using Wrapper.this, then your first assumption is indeed correct and we can see the compiler happily unwrap Wrapper:

    <synthetic> object F$Wrapper extends Object {
      final def print$extension($this: Int): Unit = 
        scala.Predef.println(scala.Int.box($this));
    

    And the call site now looks like this:

    val w: Int = 3;
    com.testing.F$Wrapper.print$extension(w)
    

    This is valid for both of your examples now, as there is no need for any dynamic dispatch on the created interface.