Puzzler 28

Pick a Value, AnyValue!

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)

Possibilities

  1. Prints:
      false
      true
    
  2. Prints:
      false
      false
    
  3. The first statement prints:
      null
    

    and the second throws an exception.

  4. Prints:
      null
      true
    

Explanation

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:

  • 0 if T is Int or one of its subrange types
  • 0L if T is Long
  • 0.0f if T is Float
  • 0.0d if T is Double
  • false if T is Boolean
  • () if T is Unit
  • null for all other types T

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.

Discussion

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
image images/moralgraphic117px.png
  1. Avoid using _ to initialize variables of abstract types to their default values if those types can later be fixed to subtypes of AnyVal.
  2. If you cannot avoid this, be aware that such variables may be null wherever the compiler does not need to unbox them to their declared type. Prefer Scala's null-safe == and ## methods over equals and hashCode and force unboxing before calling toString.

Footnotes for Chapter 28:

[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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.146.155