Mike Slinn

Type Hierarchy and Equality

— Draft —

Published 2024-08-17.
Time to read: 10 minutes.

Scala’s type hierarchy is more comprehensive than Java’s. This lecture also discusses type conversion, type widening of returned values from expressions, object equality, the Scala REPL’s :type command and provides detailed commentary on many important Scala types.

Scala’s type hierarchy is more comprehensive than Java’s. This lecture also discusses type conversion, type widening of returned values from expressions, object equality, the Scala REPL’s :type command and provides detailed commentary on many important Scala types.

The sample code for this lecture can be found in courseNotes/src/main/scala/TypeHierarchy.scala.

Scala Inheritance Hierarchy

The Scala inheritance hierarchy is shown in the following diagram (no changes at this level of abstraction were required for 100% accuracy with Scala 2.13).

Notice that:

  • All Scala values are objects, including numbers and strings.
  • All objects are subclasses of Any.
    • Any is the base type of the Scala type system, and defines final methods like ==, != and isInstanceOf.
    • Any also defines non-final methods like equals, hashCode and toString.
    • Declaring an object to be of type Any effectively removes all type checking for that object.
  • There are two main classifications of Scala types:
    • Immutable value types, such as the primitive types Byte, Short, Char, Int, Long, Float, Double, Boolean, and Unit.
      • The base class of all value types is AnyVal.
      • Bytes can be viewed as / converted to Shorts, Shorts and Chars can be viewed as / converted to Ints, Ints can be viewed as / converted to Longs, Longs can be viewed as / converted to Floats, and Floats can be viewed as / converted to Doubles.
      • Unit can also be written as ().
    • Reference types, such as Scala and Java objects. The base class of all reference types is AnyRef, which is equivalent to java.lang.Object.
      • All non-value objects (aka reference types) are subclasses of AnyRef.
      • Reference types are created using the new keyword, unless the class extends AnyVal, which means the class is a value type. As we will learn shortly, companion objects often use the default method apply to hide the use of the new keyword, but it is there nonetheless.
      • All objects that you can define are subclasses of AnyRef, including all types of Functions.
      • Null is a subtype of all reference types; it’s only instance is null.
  • Since Null is not a subtype of value types, null cannot be assigned to a variable defined as a value type.
  • Nothing is a subtype of all types. In other words, Nothing is a subtype of everything – very Zen-like!
Good Scala code
mostly consists of expressions

Type Widening

An expression returns a value. Unlike Java, which has many types of statements but few expressions, most Scala code is comprised of expressions.

Type widening happens when an expression does not return a consistent type. Lets examine how type widening affects if and match expressions now.

if-then-else

An if-then-else expression can return one type from the then clause and another type from the else clause. In this example, the variable called rightNow contains a Long with the current system time in milliseconds. The method called x tests the time to see if it is an even number, or if today is a Tuesday, and concludes with a catch-all clause. The return values are highlighted in orange, and their types are different: Boolean, String and Long. Referring to the type hierarchy diagram we can see that the common supertype of all possible return types is Any.

If-then-else expressions return the common supertype
scala> import java.util.Date
import java.util.Date 
scala> def x: Any = { | val rightNow: Long = System.currentTimeMillis | if (rightNow % 2 == 0) true else | if (new Date(rightNow).toString.contains("Tue ")) | "Today is a Tuesday" else rightNow | } x: Any
scala> x res0: Any = true
scala> x res1: Any = 1565282539351

Running the above in the REPL shows that x returns type Any due to type widening. Of course, the type returned by x each time it is invoked might actually be Boolean, Long or String.

if-then

Similar to if-then-else, if-then is an expression that returns a value, but the returned value is not useful and if used will introduce a bug. This is because an if expression without an else clause is always subject to type widening to type AnyVal.

Scala 3 fixed this problem by disallowing the assignment of an if statement without an else clause:

Shell
$ scala -explain
Welcome to Scala 3.4.2 (11.0.23, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
scala>
def useless(x: Int, y: Int) = if (x>y) x 1 warning found -- [E190] Potential Issue Warning: --------------------------------------------- 1 |def useless(x: Int, y: Int) = if (x>y) x | ^ | Discarded non-Unit value of type Int. You may want to use `()`. |----------------------------------------------------------------------------- | Explanation (enabled by `-explain`) |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | As this expression is not of type Unit, it is desugared into `{ x; () }`. | Here the `x` expression is a pure statement that can be discarded. | Therefore the expression is effectively equivalent to `()`. ----------------------------------------------------------------------------- def useless(x: Int, y: Int): Unit

The Scala 2 compiler computes the return type as AnyVal, which is an obscure clue that the computation might return Unit if the non-existant else clause is triggered:

Scala 2 if-then expressions return type AnyVal
scala> def useless(x: Int, y: Int) = if (x>y) x
useless: (x: Int, y: Int)AnyVal 
scala> useless(13, 14) res2: AnyVal = ()
scala> useless(132, 14) res3: AnyVal = 132

The nonexistent else clause has value Unit, which means “no value”. Unit is often written as (). The type of the missing else clause is added by the Scala compiler when it computes the value of the entire if-then expression.

Referring to the class hierarchy diagram we can see why the Scala compiler realizes that type AnyVal is the most specific type that can represent all possible returned values from this if/then expression.

Always include an else clause when an if/then expression is intended to return a value from a block of code.

match

We will explore match expressions in more detail in the Pattern Matching lecture later in this course, but for this lecture let’s consider match as just being a multi-way branch. This multiway branch expression either returns an Int, a Float or a Double. The common supertype is Double.

Match expressions return the common supertype
scala> def wat(x: Any): Double = x match {
   case int: Int => int
   case string: String => string.toFloat
   case _ => 1.23
}
wat: (x: Any)Double 
scala> wat(1) res4: Double = 1.0
scala> wat("1.23") res5: Double = 1.2300000190734863

Nothing and Throwable

Nothing is Scala’s bottom type; all other types are subtypes of Nothing. Type Nothing can therefore be widened to any other type.

Throwable is a Java class which in Scala can be paired with the throw keyword to throw an exception; unhandled exceptions halt the currently running program. The JVM defines two subclasses of Throwable: Error and Exception. If this information is news to you please read the Throwable Javadoc. Another good source of information is the chapter on Exceptions in the JVM Specification.

The sys.error method prints a message to standard error and exits the program by throwing an Exception, specifically a RuntimeException. If you invoke sys.error in the REPL the message is displayed and any computation that had been underway is halted, however the REPL does not exit. Here it is in action:

Invoking sys.error
scala> sys.error("Oopsie!")
java.lang.RuntimeException: Oopsie!
  at scala.sys.package$.error(package.scala:29)
 ... 28 additional lines of the stacktrace not shown 

Because sys.error is declared as returning type Nothing you can invoke sys.error in an assignment to any type, and the result type will be determined by the other, non-Nothing type. For example, the then clause of this if statement has type Int, and the else clause has type Nothing; this means the returned type of the entire if expression is Int.

sys.error return type
scala> val meaning: Int = if (true) 42 else sys.error("Oopsie!")
meaning: Int = 42 

Because sys.error ends the program it never returns a value, so it should seem natural to be able to add in a call to sys.error anywhere in your program without affecting return types.

Similarly, in Scala the throw keyword is defined to have type Nothing. Again this means that return types of expressions are unaffected by exceptions that are thrown inside those expressions.

As we will learn in the Try and try/catch/finally lecture later in this course, in Scala each of the try and case clauses contribute to the computed return type. In this next code example an Exception will be thrown if there is a problem converting string to an Int. Because that exception is merely returned by the case clause without being handled, and is just rethrown, the try statement in this code example does not do anything useful. The point of this code example is to show that because of type widening the type returned by the try statement is Int, and the type returned by the case clause is Nothing, so the returned type for the entire try / catch expression is Int.

Throw return type
scala> def try1(string: String): Int =
     |   try {
     |     string.toInt
     |   } catch {
     |     case e: Exception => throw e
     |   }
try1: (string: String)Int 
scala> try1("123") res6: Int = 123

The try2 method builds on the try1 code example. try2 returns Double because both catch cases return Double, so the Int computed by the try clause is widened to Double. The first catch case handles NumberFormatExceptions by returning the constant 42.0, while the second catch case attempts to convert string to a Double. If that fails then an uncaught exception will be thrown.

Widened return type
scala> def try2(string: String): Double =
     |   try {
     |     string.toInt
     |   } catch {
     |     case _: NumberFormatException =>
     |       42.0
     |
     |     case _: Exception =>
     |       string.toDouble
     |   }
try2: (string: String)Double 
scala> try2("12.3") res7: Double = 12.3
scala> try2("asdf") res8: Double = 42.0

Exercise – Type Conversion and Type Widening

Taking the try2 example one step further, here is try3. Please explain why try3 returns Double.

def try3(string: String): Double =
  try {
    string.toInt
  } catch {
    case _: Exception =>
      try {
        string.toDouble
      } catch {
        case e: Exception =>
          throw e
      }
  }

Solution

As the title of this excercise suggests, two things are going on in this code example: type conversion and type widening.

  1. The type of the try clause is declared to be Int.
  2. The type of the nested try clause is Double.
  3. The type of the innermost catch case is Nothing.

Ints are converted to Doubles.

The common supertype of Double and Nothing is Double, so the return type of try3 is Double.

Declaring a Method that Returns Nothing

Methods that return Nothing are only useful for terminating a program or throwing an exception.

Here is the Scala source code for sys.exit:

sys.exit source
def exit(status: Int): Nothing = {
  java.lang.System.exit(status)
  throw new Throwable()
}

The method calls java.lang.System.exit, which never returns. The expression that follows throws a new Throwable, but even though that is never executed its return type establishes the type of the exit method because that is the last statement.

The run time libraries for Perl, PHP and Python all define a method called die. Here is an example of how to define and use a similar method for Scala, also called die, that returns Nothing. Notice that the variable called freedom has type Int, and that the program ends if the else clause executes.

Scala die method
def die(message: String): Nothing = {
  Console.err.println(message)
  Console.err.flush()
  sys.exit(1)
}

val freedom: Int = if (false) 99 else die("Help! I’m trapped in a computer!")

You can run the above code by typing:

Running the Scala die method
$ sbt "runMain NothingDoing"
Help! I’m trapped in a computer!
Exception: sbt.TrapExitSecurityException thrown from the UncaughtExceptionHandler in thread "run-main-0"

Object Equality

In Scala, all types except Arrays use value equality as defined by the type’s equals method, and manifested by == and != operators, not reference equality like Java or C#. Remember that the abstract class Any defines the equals, == and != methods, and those methods are often overriden by subtypes.

Methods that define equality
def equals(arg0: Any): Boolean
final def ==(arg0: Any): Boolean
final def !=(arg0: Any): Boolean

Almost all Scala types use value equality. For example, Strings are compared using values and not references:

Almost all Scala types are compared using values and not references
scala> val s1 = "hi"
s1: String = hi 
scala> val s2 = s1 s2: String = hi
scala> assert(s1==s2) scala> assert(s1=="hi")

Scala handles Array equality differently than for all other Scala types, using reference equality and not value equality. This means that two arrays with the same data do not compare equal. For example:

Arrays are compared using references and not values
scala> val a1 = Array(1, 2)
a1: Array[Int] = Array(1, 2) 
scala> val a2 = a1 a2: Array[Int] = Array(1, 2)
scala> assert(a1==a2)
scala> assert(a1==Array(1, 2)) java.lang.AssertionError: assertion failed at scala.Predef$.assert(Predef.scala:151) ... 33 additional lines of output elided

The Rest of the Equality Contract

As is the case with Java, if you define equals you must also define hashCode.

Let’s say we define a Dog class and a Hog class to each have a property called name, and we want to be able to compare Dog instances with other Dog instances using the Dog.name property. We also want to be able to compare Hog instances with other Hog instances via the Hog.name property. However, we need to disallow the comparison of Dog instances with Hog instances. If regular Scala classes are used, we would need to use isInstanceOf to validate whether the comparison makes sense or not, like this:

class Dog(val name: String) {
  override def equals(that: Any): Boolean = canEqual(that) && hashCode==that.hashCode

  override def hashCode = name.hashCode

  def canEqual(that: Any) : Boolean = that.isInstanceOf[Dog]
}

class Hog(val name: String) {
  override def equals(that: Any): Boolean = canEqual(that) && hashCode==that.hashCode

  override def hashCode = name.hashCode

  def canEqual(that: Any) : Boolean = that.isInstanceOf[Hog]
}

val dog = new Dog("Fido")
val hog = new Hog("Porky")

println(s"Should a dog be compared to a hog? ${dog.canEqual(hog)}")
println(s"Comparing ${dog.name} with ${hog.name} gives: ${dog==hog}")

Output is:

Output
Should a dog be compared to a hog? false
Comparing Fido with Porky gives: false

The Strict Equality Operator Prevents Bugs

A source of bugs that I personally encounter more often than I care to admit is when comparing a type with an Option of that type. Continuing our example above:

Continuing the above example
scala> val maybeDog: Option[Dog] = Some(dog)
maybeDog: Option[Dog] = Some(Dog@2168ae) 
scala> dog == maybeDog // returns false, but the comparison should not be made! res4: Boolean = false

Here is one possible way of dealing with the problem. We enhance our Dog class with an strict equality operator ===. This performs a runtime check.

object MaybeDog2 extends App {
  class Dog2(val name: String) {
    override def equals(that: Any): Boolean = canEqual(that) && hashCode==that.hashCode

    override def hashCode = name.hashCode

    def canEqual(that: Any) : Boolean = that.isInstanceOf[Dog2]

    def ===(that: Any): Boolean =
      if (canEqual(that)) this==that else {
        println(s"ERROR: ${getClass.getName} should not be compared to a ${that.getClass.getName}")
        false
      }
  }

  val dog2 = new Dog2("Fido2")
  val maybeDog2 = Some(dog2)
  println(s"Comparing dog2 with maybeDog2: ${dog2===maybeDog2}")
}

When we use === to compare a Dog2 with Option[Dog2] we get a warning that the comparison is invalid. This definitely indicates that we have a bug in our code:

Output
ERROR: MaybeDog2$Dog2 should not be compared to a scala.Some
Comparing dog2 with maybeDog2: false

In the Case Classes lecture we will explore how Scala’s case classes automatically define canEqual, which informs us if two objects can logically be compared. This causes the check to be made at compile time instead of at runtime.

REPL’s :type

The Scala REPL has a :type command that displays the type of an expression. Here are examples of how it can be used:

The :type REPL command
scala> :type Array(1, 2)
Array[Int] 
scala> :type die("asdf") Nothing
scala> :type dog Dog
scala> :type hog Hog

The :type command requires an expression to be evaluated. Merely providing a method name is insufficient:

The :type REPL command (continued)
scala> :type wat
^ error: missing argument list for method wat Unapplied methods are only converted to functions when a function type is expected. You can make this conversion explicit by writing `wat _` or `wat(_)` instead of `wat`. scala> :type wat("x") Double

:type only analyses instances of types, not types themselves. Asking for the type of built-in classes such as Array, Int and PartialFunction just returns a tautalogy:

The :type REPL command (continued)
scala> :type Array
Array.type 
scala> :type Int Int.type
scala> :type PartialFunction PartialFunction.type

User-defined types must be instantiated before :type will attempt to analyse their instance’s type:

The :type REPL command (continued)
scala> :type Dog
^
             error: not found: value Dog 
scala> :type new Dog Dog

The -v option provides more verbose output, including the internal type hierarchy for the computed value of the expression. Sometimes this might be insightful, but since this is just a dump of sbt’s undocumented internal data structures the output may not be as helpful as you might wish.

The :type REPL command (continued)
scala> :type Some("a")
Some[String] 
scala> :type -v Some("a") // Type signature Some[String] // Internal Type structure TypeRef( TypeSymbol( final case class Some[+A] extends Option[A] with Product with Serializable ) args = List( TypeRef( TypeSymbol( final class String extends Serializable with Comparable[String] with CharSequence ) ) ) )

Specially Handled Types

This section is inspired by Sébastien Doeraene’s answer to this Stack Overflow posting. Sébastien is the author of Scala.js. I provide links to types that are discussed in this course or the follow-on course, Intermediate Scala.

Special for the Type System

The following types are crucial to Scala’s type system. They have an influence on how type checking itself is performed. All these types are mentioned in the Scala Language Specification (or at least, they should be).

Known to the compiler as the desugaring of some language features

The following types are not crucial to the type system. They do not have an influence on type checking. However, the Scala language does feature a number of constructs which desugar into expressions of those types. These types are described in the Scala Language Specification.

Known to the implementation of the language (Supplemental)

This information is outside the scope of this course, and is therefore marked supplemental.

The previous categories are handled by early (front-end) phases of the compiler, and are therefore shared by Scala/JVM, Scala.js and Scala Native. This category is typically known of the compiler back-end, and so potentially have different treatments. Note that both Scala.js and Scala Native do try to mimic the semantics of Scala/JVM to a reasonable degree.

Those types might not be mentioned in the Scala Language Specification, at least not all of them.

Here are those where the back-ends agree (re Scala Native, to the best of my knowledge):

  • All primitive types: Boolean, Char, Byte, Short, Int, Long, Float, Double, Unit.
  • scala.Array; discussed in the Mutable Collections lecture of the Intermediate Scala course.
  • Cloneable (currently not supported in Scala Native, see #334)
  • String and StringBuilder (mostly for string concatenation)
  • Object, for virtually all its methods

And here are those where they disagree:

  • Boxed versions of primitive types (such as java.lang.Integer)
  • Serializable
  • java.rmi.Remote and java.rmi.RemoteException
  • Some the annotations in scala.annotation.* (e.g., strictfp)
  • Some stuff in java.lang.reflect.*, used by Scala/JVM to implement structural types

Also, although not types per se, but a long list of primitive methods are also handled specifically by the back-ends.

Platform-specific types (Supplemental)

This information is outside the scope of this course, and is therefore marked supplemental.

In addition to the types mentioned above, which are available on all platforms, non-JVM platforms add their own special types for interoperability purposes.

Scala.js-specific types

See JSDefinitions.scala.

  • js.Any: conceptually a third subtype of Any, besides AnyVal and AnyRef. They have JavaScript semantics instead of Scala semantics.
  • String and the boxed versions of all primitive types (heavily rewritten--so-called "hijacked"--by the compiler).
  • js.ThisFunctionN: their apply methods behave differently than that of other JavaScript types (the first actual argument becomes the thisArgument of the called function).
  • js.UndefOr and js.| (they behave as JS types even though they do not extend js.Any).
  • js.Object (new js.Object() is special-cased as an empty JS object literal {}).
  • js.JavaScriptException (behaves very specially in throw and catch).
  • js.WrappedArray (used by the desugaring of varargs).
  • js.ConstructorTag (similar to ClassTag).
  • The annotation js.native, and all annotations in js.annotation.*

Plus, a dozen more primitive methods.

Scala Native-specific types

See NirDefinitions.scala.

  • Unsigned integers: UByte, UShort, UInt and ULong.
  • Ptr, the pointer type.
  • FunctionPtrN, the function pointer types.
  • The annotations in native.*
  • A number of extra primitive methods in scala.scalanative.runtime.

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.