Cannot be cast to class scala.runtime.BoxedUnit

#1

Hi!
I encountered an unexpected behavior at runtime.
Consider the following code:

def execute[R](): R = (new Object).asInstanceOf[R]
val r1: Unit = execute[Int]() // SUCCESS!
val r2: Unit = execute[Nothing]() // class java.lang.Object cannot be cast to class scala.runtime.BoxedUnit
val r3: Unit = execute[Unit]() // class java.lang.Object cannot be cast to class scala.runtime.BoxedUnit

It seems that when we have a val expecting Unit, and we try to assign any other type to it, the value will be discarded (like with Int). However, if we specify the type of the value as Unit or Nothing, Scala will try to convert it to a scala.runtime.BoxedUnit, which would result in an error in the case above.
My question is why does Scala try to convert Unit or Nothing to scala.runtime.BoxedUnit instead of discarding the value, like with any other type?

System details:

  • macOS Mojave 10.14.5
  • scala 2.13
  • jdk 11.0.3
#2

Additionally:

val n = execute[Nothing]() //java.lang.ClassCastException: java.lang.Object cannot be cast to scala.runtime.Nothing$
val u = execute[Unit]() //java.lang.ClassCastException: java.lang.Object cannot be cast to scala.runtime.BoxedUnit
val uu = (new Object()).asInstanceOf[Unit] //works

all also reproducible in 2.12.8

I’m not sure what the bug is exactly, if anything, but execute[Nothing]() should never be allowed to return anything if anything can be Just or Right, even in the light of asInstanceOf.The buggiest thing there, IMO, are the parts that don’t throw ClassCastExceptions.

1 Like
#3

I guess that in the given code following things happen:

// R is erased totally, so asInstanceOf does nothing in runtime, but still affects scalac typechecker
def execute[R](): R = (new Object).asInstanceOf[R]

// scalac sees that execute[Int]() returns Int, which is not a subclass of Unit, so it must drop it
val r1: Unit = execute[Int]()

// scalac sees that execute[Nothing]() returns Nothing, which is a subclass of Unit, so it tries to use it as Unit
val r2: Unit = execute[Nothing]()

// scalac sees that execute[Unit]() returns Unit, so it tries to use it as Unit
val r3: Unit = execute[Unit]()

Therefore there’s no bug in the compiler. Instead, there’s a bad combination of type erasure and runtime type casts.

I’ll guess again. My hunch is that discarding the value is tried only after assignment doesn’t typecheck. Is that useful at all? I don’t know.

3 Likes
#4

I think you’re right.

def execute[R](): R = (new Object).asInstanceOf[R]
// Behavior is similar here
val r1: Int = execute[Int]() // class java.lang.Object cannot be cast to class java.lang.Integer
val r2: Unit = execute[Unit]() // class java.lang.Object cannot be cast to class scala.runtime.BoxedUnit

r1 can be used as Int, cast is necessary.
r2 can be used as Unit. But there is only one value of type Unit, therefore no need to cast.

val r3: Unit = execute[Int]()

r3 does not type check. But we see no warning from the compiler. And scala runtime is smart enough to discard the result. Such behavior is probably necessary for operating without explicit return. I can imagine the following scenario:

def getInt(): Int = 1
def foo(): Unit = getInt
foo()

If there was no discarding mechanism, the compiler would have to raise an error, since Int is not a subtype of Unit.

val r4: Int = execute[Unit]()// ERROR: Type mismatch!

r4 does not type check. But in this case compiler knows that Unit cannot be used as Int and raises an error.

I still think cast Unit -> Unit should not happen, because:

  1. There is no real value to it. Cast to Unit is equivalent to discarding the value from the client perspective.
  2. It would be consistent with the discarding mechanism that works fine without generics:
      // During the cast the value will be discarded in all cases. Sometimes, we'll see a warning.
      val u1: Unit = ()
      val u2: Unit = new Object
      val u3: Unit = true // warning: a pure expression does nothing in statement position
      val u4: Unit = 1 // warning: a pure expression does nothing in statement position
#5

The real questions IMO are why

(new Object()).asInstanceOf[Unit]

doesn’t throw a ClassCastException, and why

val u2: Unit = new Object

doesn’t emit a discard warning

It’s worth noting that there are no casts in your last examples at all.

  val u1: Unit = ()
  val u2: Unit = new Object //I have no idea what's happening here
  val u3: Unit = true // warning: a pure expression does nothing in statement position
  // this is equivalent to val u3: Unit = { true; () }. There is no casting going on.
  val u4: Unit = 1 // warning: a pure expression does nothing in statement position
 //  this is equivalent to val u4: Unit = { 1; () }. there is no casting going ong
#6

Note that there is nothing in the Scala spec that demands it throw a ClassCastException at any point. When an asInstanceOf is supposed to be successful, then Scala must not throw. But if it is not supposed to be successful, Scala can do whatever it wants.

In practice it often chooses to do the least amount of compile-time and/or run-time work sufficient to convince the JVM, or JavaScript, to pursue.

2 Likes
#7

You have to ask:

$ scala -Wvalue-discard
Welcome to Scala 2.13.0 (OpenJDK 64-Bit Server VM, Java 12.0.1).
Type in expressions for evaluation. Or try :help.

scala> val u: Unit = new Object
                     ^
       warning: discarded non-Unit value
u: Unit = ()
2 Likes
#8

According to the Value Discarding section of the Scala specs:

If e has some value type and the expected type is Unit , e is converted to the expected type by embedding it in the term { e; () } .

Therefore, I would expect this expression:

val u: Unit = execute[Unit]()

to be rewritten by the compiler to:

val u: Unit = { execute[Unit](); () }

In such case, no conversion error occurs and the value discarding behaviour is consistent.

But it seems that because:

Implicit conversions can be applied to expressions whose type does not match their expected type

and since we have Unit => Unit, implicit conversion does not happen.

And yet, the behaviour in r3 does not confirm to my logic:

def execute[R](): R = (new Object).asInstanceOf[R]

val r1: Unit = execute[Unit]() // class java.lang.Object cannot be cast to class scala.runtime.BoxedUnit
val r2: Int = execute[Int]() // class java.lang.Object cannot be cast to class java.lang.Integer

val r3: Unit = (new Object).asInstanceOf[Unit] // SUCCESS!
val r4: Int = (new Object).asInstanceOf[Int] // class java.lang.Object cannot be cast to class java.lang.Integer

Maybe r3 it’s treated as a special case. But then, why not do the same in r1? In my opinion, when the expected type is Unit it makes sense to discard the value regardless of its type.

#9

I just saw the blue box saying let’s welcome y’all to the community, so welcome!

The icon is two manly hands man-handling, one hand clearly the superior hand.

I’d propose an icon based on the famous Sistine chapel God/man hands. Offhand, so to speak, I don’t remember what it represents. Is it when Adam says, I’m lonely! or when God says, You know what tickles? and then demonstrates.

So, your question is, after we lie to the compiler and claim that an Object is a (boxed) R, why do we get away with the lie in one case but not the other? The one case (r3) is just an expression that is Unit. There is nothing more to say, you said it was a Unit! The other case, though, is polymorphic, which is Greek for liar, which means we must “trust but verify”. Actually, we just verify.

3 Likes
#10

After compiling the following Scala code:

  def execute[R](): R = (new Object).asInstanceOf[R]

  def main(args: Array[String]): Unit = {
    val u1: Unit = (new Object).asInstanceOf[Unit]
    val i1: Int = (new Object).asInstanceOf[Int]
    val s1: String = (new Object).asInstanceOf[String]

    val u2: Unit = execute[Unit]()
    val i2: Int = execute[Int]()
    val s2: String = execute[String]()
  }

and decompiling it to Java:

  public <R> R execute() { return (R)new Object(); }
  
  public void main(String[] args) {
    new Object(); BoxedUnit u1 = BoxedUnit.UNIT;
    int i1 = BoxesRunTime.unboxToInt(new Object());
    String s1 = (String)new Object();
    
    BoxedUnit u2 = (BoxedUnit)execute();
    int i2 = BoxesRunTime.unboxToInt(execute());
    String s2 = (String)execute();
  }

compiler’s behaviour is consistent when we have primitive and reference types (i1 and i2, s1 and s2) but it isn’t when we have Unit.