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?
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.
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.
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:
There is no real value to it. Cast to Unit is equivalent to discarding the value from the client perspective.
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
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
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.
$ 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 = ()
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.
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.
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.
6.26 begins: “Implicit conversions can be applied to expressions whose type does not match their expected type” (and other contexts).
You agreed with tarsa that this is a precondition for the “value discard” conversion.
(Edit: sorry, glancing back I see you quoted this precondition.)
Maybe you’re saying that BoxedUnit is different from Unit, but that is an artifact of the implementation. In Scala, you only see Unit and Int, not BoxedUnit and Integer.
In particular, if some generic method hands you an Object you think should be of type Unit (or any other boxed type), you must verify before handling the reference.
Although I sympathize with your idea that a unit is a unit, it’s also possible that a programmer error will result in a reference type that is not a boxed unit. (The example code is such an error.) Rather than ignoring that mistake, you want to discover it early.
There are previous email threads about boxed unit. I think Lukas made the improvements, at least a few years ago. The questions around java interop were not trivial. I think that was on the old “internals” email list, but I don’t really remember. I remember it was an interesting discussion.
It’s a reminder that some smart people – I was going to say something appreciative about how much work goes into scalac, but there is a large spider sauntering down the wall.
object App {
def execute[R](): R = (new Object).asInstanceOf[R]
def doMatch(): Unit = {
// match is an expression in Scala (i.e., it always results in a value). Therefore, the `execute` call is in fact `val matchResult = execute/.../`
() match {
case _ => {
// The inferred type will be `Unit`, because `doMatch` must return a `Unit`.
execute() // class java.lang.Object cannot be cast to class scala.runtime.BoxedUnit
}
}
}
def main(args: Array[String]): Unit = {
doMatch()
}
}
Unit means I don’t want to return anything from the method. I don’t care what execute returns, just throw it away. By the way, the execute method is part of Java code in my project.
It’s used in other places where I do need the returned value. In that particular case, I’ve shown above, the method is executed only for its side effects.
Right, but that’s irrelevant – execute() is crashing while it runs. Whether the result gets thrown away or not doesn’t matter. “Thrown away” doesn’t mean “doesn’t run”, and you are doing an illegal cast inside of execute().
Seriously – what surprises me isn’t that it’s failing for Unit, it’s that it succeeds on Int. You’re asking for a clearly-illegal cast (casting Object to AnyVal). The fact that the compiler sometimes magically makes that succeed due to boxing isn’t terribly obvious, and it isn’t obvious why it would necessarily always work…
If you make it illegal to use Unit as a type argument then you’re forbidding Function1[T, Unit] for which sugared form is T => Unit. Also you would forbid IO[Unit] and many other useful types.
Automatic () is very convenient for prototyping. If I e.g. change return type of a method m from String to Unit then in many places I don’t need to refactor code, e.g I can still use m in List.map, I can still assign the result of invoking m to a variable, etc