4
votes

Scala superimposes a very elegant class hierarchy over Java's type system, balooning out from Any at the top, into AnyRef and AnyVal to cover Java's Objects and primitives respectively, then finally converging, collapsing the reference types onto Null and all types onto Nothing. As I understand it, Nothing is a subtype of everything; Null a subtype of all subtypes of AnyRef/java.lang.Object. [ see http://www.scala-lang.org/node/128 ]

However, there seem to be a few irregularities, a few places where it does not work to simply think of all Scala types as elements of a seamless type hierarchy. I find this irksome, and want to understand the places where I might be surprised.

So far, I know of a few irregularities:

1) Although Null is a subtype of AnyRef, calling null.isInstanceOf[AnyRef] (or other subtypes of AnyRef) returns false. I suspect this was chosen to be consistent with the behavior of Java's instanceof operator.

2) Everything is covariant to Nothing, regardless of variance annotations. If I have a method that returns a type T that is not marked covariant, I can override that method to return type Nothing. [NOTE: this claim is mistaken, see answers and comments below!]

3) I can't apply isInstanceOf to the type AnyVal [ See Why can AnyVal not be used in an isInstanceOf check? and How to test a value on being AnyVal? ]

4) It is illegal to ask whether something isInstanceOf[Null], which is a perfectly coherent thing to ask (although not particularly necessary, since "myVar == null" would give the same answer)

Are there other examples of irregularities or special cases in Scala's type hierarchy? I feel like these are worth learning and understanding to avoid unwelcome surprises.

1
2) isn't true. The first sentence is not supported by the second. Try trait T[A]; case class C[A]() extends T[A]; val l:T[String] = C[Nothing](). It won't work due to invariance. If you change it to +A it does work.Kim Stebel
null.isInstanceOf[Null] is false, because null is not an instance.Ricky Clarkson
Kim -- Yeah. I clearly messed up the description of (2). I saw some weirdness that I wrongly took to be a variance issue. It's not. Still, there is some weird behavior I don't get, see my comment to axel22's response below.Steve Waldman
Ricky -- why wouldn't we think of null as a singleton instance of the type Null? in every respect, that's what it seems to be. we define it away because we know that it doesn't imply an Object on the JVM's heap? seems like an implementation detail. because defining it as "not an instance" matches the behavior of instanceof?Steve Waldman

1 Answers

7
votes

1) "A string is a subtype of AnyRef".isInstanceOf[AnyRef] returns true. This is true for other subtypes of AnyRef as well, except for Null. The only irregularity there is done to be consistent with Java, as you said.

2) If B is a subtype of A, that is B <: A, then you can always override a method:

def foo: A = ...

to:

override def foo: B = ...

This is called refining the return type, and is always allowed. Since Nothing is a subtype of every other type (Nothing <: A for all A), you can always refine your return type to Nothing (e.g. by throwing an exception in the body of the method). This is a pretty regular property. The return type covariance is not directly related to variance annotations on type parameters.

3) The other questions cover this nicely.

4) This is because the Null type does not exist in the Java runtime. I guess if you wanted to emulate this, you could create your own instanceOf method - you would first have to check if the argument is null, otherwise, do the normal isInstanceOf check.

There are other irregularities, yes. See for example: If an Int can't be null, what does null.asInstanceOf[Int] mean?

Arrays are another example, where you may pay the uniformity of generic arrays with boxing/unboxing or instanceof checks at runtime. The new Array[Any] is translated into an object array - storing an integer into the array will result in boxing it. Whenever you use an Array[T], where T has no upper bound, the array will be patterned matched against the correct runtime array type every time you index an element.

To better understand how you might be surprised it's useful to think in terms of how these constructs are translated to the JVM where there is a notion of primitive and reference types, boxing/unboxing and different array classes.