Value classes, universal traits and the necessity of instantiation
Asked Answered
H

1

6

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?

Holytide answered 6/11, 2017 at 15:46 Comment(0)
W
3

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.

Woodworker answered 6/11, 2017 at 16:25 Comment(13)
Interesting. I'd actually not really meant to specifically consider the fact it was a call to print(this). Presumably had I chosen println(underlying) no instantiation would have been necessary?Holytide
@Holytide I was wondering if you intentionally wanted this or not. I'll update the answer.Woodworker
But it causes a call to Int.box? Pfft!Holytide
@Holytide $this now refers to the local value $this: Int in the method signature, not the this pointer. No allocations necessary now.Woodworker
But your impl of print$extension is scala.Predef.println(scala.Int.box($this));Holytide
So the Int gets boxed into a java.lang.IntegerHolytide
@Holytide print$extension($this: Int). The only boxing is because we need an Integer instance for println, not an Int.Woodworker
@Holytide Yes, it does, but no allocation of Wrapper. This is a limitation of println, again since it needs Any.Woodworker
"As long as we reference this inside or outside the value class, we're trapped." I think this is wrong (or I don't understand what you mean). The problem is that here "a value class is treated as another type" (namely, a Wrapper as an Any); whether it's this or not, doesn't matter. And passing this to a method accepting a Wrapper is fine.Guanajuato
@Alexey Romanov this is problematic in the context of println because of Any, is what I meant. I'll clarify.Woodworker
But why does Int need to be boxed to be considered an Any? Int <: AnyVal <: AnyHolytide
I've asked this separately here: #47157041Holytide
Doesn't def print(): Unit = println(underlying.toString) work to stop the wrapping of underlying? IOW, doesn't the use of toString enables the compiler to see the "intention" and make the static call as it does in other similar places?Saloma

© 2022 - 2024 — McMap. All rights reserved.