Scala's abstract type members allow you to define base classes without immediately committing to specific implementation types. You could, for instance, define a Recipe without having to decide up front how precise you need the amounts to be, i.e., which numeric type to use for quantities:
trait Recipe { type T <: AnyVal def sugarAmount: T def howMuchSugar() { println(s"Add ${sugarAmount} tablespoons of sugar") } }
val approximateCake = new Recipe { type T = Int val sugarAmount = 5 }
scala> approximateCake.howMuchSugar() Add 5 tablespoons of sugar
val gourmetCake = new Recipe { type T = Double val sugarAmount = 5.13124 }
scala> gourmetCake.howMuchSugar() Add 5.13124 tablespoons of sugar
If you want to initialize a variable of the abstract type in the base class, you cannot in general use a specific value, since the type itself is not yet known. You can, however, initialize a var to the default value for its type by setting it to _ (i.e., underscore).[1]
What is the result of executing the following code in the REPL?
trait NutritionalInfo { type T <: AnyVal var value: T = _ } val containsSugar = new NutritionalInfo { type T = Boolean }
println(containsSugar.value) println(!containsSugar.value)
false true
false false
null
and the second throws an exception.
null true
You may wonder whether the value of Booleans initialized to their default by setting them to _ is, for some reason, not false. Or you may suspect that the variable starts out as null despite being a Boolean, and throws a NullPointerException when you try to negate.
In fact, this is almost the case—but not quite. The correct answer is number 4:
scala> println(containsSugar.value) null
scala> println(!containsSugar.value) true
How can a Boolean value be null? After all, the compiler "knows" at the moment of initialization that the variable will be an AnyVal. Even if the compiler internally uses null, for whatever reason, why is this visible to the program? And why do you then not see a NullPointerException when you try to negate the value?
Surprising as it may seem, the fact that your AnyVal variable is initialized to null is not a compiler trick of some kind. It is stated explicitly in the language specification:[2]
The default value depends on the type T as follows:
Even though the compiler knows at the point of initialization that your variable will be some subtype of AnyVal, it does not know which type. According to the language specification, null is indeed the appropriate default value in that case.
By the time you attempt to negate the value, the compiler knows, of course, that it is a Boolean. Indeed, you have to treat it as a Boolean in order to be able to invoke the unary_! method (Scala's boolean negation) on it. This method is defined on scala.Boolean, not inherited from any of Boolean's supertypes.
When calling unary_!, the compiler handles the task of treating the underlying java.lang.Object (with value null) as a Boolean by automatically unboxing it using scala.runtime.BoxesRunTime.unboxToBoolean. This method handles an underlying value of null by returning false, the default value for Booleans in Scala. Negating that value then prints "true". So far, so unspectacular.
What about the first println statement, though? At that point, the compiler also knows that the value is a Boolean. Why then do you see null, rather than the expected Boolean default value false?
It turns out that the cause here is the type of the argument expected by println, which is Any. When the compiler encounters the expression, println(containsSugar.value), it checks whether an instance of type java.lang.Object (the type of containsSugar.value) can be passed to println. Since println only expects an Any, this works just fine. There is no need to treat the value as a Boolean in this case, so no unboxing is applied and the underlying null value is printed.
Surprisingly, this is also the case if you force the value to be treated as an AnyVal. Only if you cause it to be treated as a Boolean is unboxing applied:
def printAnyVal(a: AnyVal) { println(a) }
scala> printAnyVal(containsSugar.value) null
def printBoolean(b: Boolean) { println(b) }
scala> printBoolean(containsSugar.value) false
The compiler's refusal to unbox if the value can be treated as an Any can cause surprising NullPointerExceptions when invoking methods inherited from java.lang.Object:
scala> containsSugar.value equals false java.lang.NullPointerException ...
scala> containsSugar.value.hashCode java.lang.NullPointerException ...
scala> containsSugar.value.toString java.lang.NullPointerException ...
Instead of equals and hashCode, you can use Scala's null-safe versions:
scala> containsSugar.value == false res11: Boolean = true
scala> containsSugar.value.## res12: Int = 1237
In the case of ==, the unboxing of containsSugar.value is triggered by the presence of a ==(x: Boolean) method on class Boolean, which is more specific[3] than the ==(arg0: Any) variant that Boolean inherits from Any. This forces the compiler to treat your value as a Boolean.[4]
If you really wanted to, you could force the compiler to unbox your value by using a type ascription:
scala> (containsSugar.value: Boolean) equals false res13: Boolean = true
scala> (containsSugar.value: Boolean).hashCode res14: Int = 1237
Note that this is not a cast, so there is no loss of type safety.
A more rigorous solution is to avoid initializing the variable until the specific AnyVal subtype is known. This allows the compiler to choose the appropriate default value:
trait NutritionalInfoNoDefault { type T <: AnyVal var value: T } val containsSugar2 = new NutritionalInfoNoDefault { type T = Boolean var value: T = _ }
scala> containsSugar2.value equals false res15: Boolean = true
scala> containsSugar2.value = true containsSugar2.value: containsSugar2.T = true
|
[1] Odersky, The Scala Language Specification, Section 4.2. [Ode14]
[2] Odersky, The Scala Language Specification, Section 4.2. [Ode14]
[3] Odersky, The Scala Language Specification, Section 6.26.3. [Ode14]
[4] See Puzzler 22 for a more detailed discussion of unboxing.