javascalagenericsparameterized-types

Scala generics Confusion


I have some generic/OOP Scala code (not useful, just classroom excercises)

I have a Container interface, "IterableContainer", that takes in and returns objects whose type is a subclass of AnyRef. It has a concrete sub-class that also takes in and returns objects whose type is a subclass of AnyRef.

  trait IterableContainer[Type <: AnyRef] {

    def length: Int

    def getAt(index: Int): Type

    def append(element: Type)
  }

  class IterableArrayList[T <: AnyRef]() extends IterableContainer[T] {

    val underlyingContainer = new ArrayBuffer[T](16)

    override def length: Int = {
      return underlyingContainer.length
    }

    override def getAt(index: Int): T =
    {
      if (index < underlyingContainer.length) {
        return underlyingContainer(index)
      } else {
        return null // Expression of type T does not conform to expected type. 
      }
    }

    override def append(element: T) = {
      underlyingContainer :+ element // :+ means append
    }
  }

Can anyone explain why I cannot return null when T is explicitly being stated as a an object of a type that extends AnyRef?

Also, if you understand Scala's generics better than I do, please explain further - they really make no sense to me (relative to C++ generics).


Solution

  • The problem is that T <: AnyRef does not imply that T >: Null.

    I believe the only counter-example is Nothing.

    Adding an additional (lower) type bound on your IterableArrayList's type parameter makes the compiler happy:

    class IterableArrayList[T >: Null <: AnyRef]() extends IterableContainer[T] {
    

    Now the compiler knows that T is a subtype of AnyRef and a supertype of Null.


    Now, you may be thinking to yourself, "Doesn't this cause all sorts of problems for writing library code in Scala? People must have to add T >: Null everywhere!"

    However, the answer is no.

    Why? Because Scala programmers pretty much never use null.

    Let's look at List as an example. There are two ways to get the first element of a list:

    def head: A (scaladoc)

    def headOption: Option[A] (scaladoc)

    If you call head on an empty list, you get an exception. If you call headOption on an empty list, then you get None.

    That's the usual strategy for Scala libraries: either you throw an exception, or you return an Option type—you never return null.

    Example using Option[T]:

    override def getAt(index: Int): Option[T] =
      if (index < underlyingContainer.length) {
        Some(underlyingContainer(index))
      } else {
        None
      }
    

    Example using an exception:

    override def getAt(index: Int): T = underlyingContainer(index)
    

    Note that the underlying ArrayBuffer already throws an IndexOutOfBoundsException if the index is invalid. This also has the added advantage of working correctly if the user passes in a negative index... (You might want to add a 0 <= index to your condition if you go with using Option.)