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/.
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. - There are two main classifications of Scala types:
-
Immutable value types, such as the primitive types
Byte,Short,Char,Int,Long,Float,Double,Boolean, andUnit.-
The base class of all value types is
AnyVal. -
Bytes can be viewed as / converted toShorts,Shorts andChars can be viewed as / converted toInts,Ints can be viewed as / converted toLongs,Longs can be viewed as / converted toFloats, andFloats can be viewed as / converted toDoubles. Unitcan also be written as().
-
The base class of all value types is
-
Reference types, such as Scala and Java objects.
The base class of all reference types is
AnyRef, which is equivalent tojava.lang.Object.- All non-value objects (aka reference types) are subclasses of
AnyRef. -
Reference types are created using the
newkeyword, unless the class extendsAnyVal, which means the class is a value type. As we will learn shortly, companion objects often use the default methodapplyto hide the use of thenewkeyword, but it is there nonetheless. - All objects that you can define are subclasses of
AnyRef, including all types ofFunctions. -
Nullis a subtype of all reference types; it’s only instance isnull.
- All non-value objects (aka reference types) are subclasses of
-
Immutable value types, such as the primitive types
- Since
Nullis not a subtype of value types,nullcannot be assigned to a variable defined as a value type. -
Nothingis a subtype of all types. In other words,Nothingis a subtype of everything – very Zen-like!
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.
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:
$ 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> 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.
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:
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.
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.
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.
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.
- The type of the try clause is declared to be
Int. - The type of the nested try clause is
Double. - 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:
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.
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:
$ 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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.
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).
-
Any,AnyRef,AnyVal,Null,Nothing: the five types that sit at the top and bottom of Scala’s type system. -
scala.FunctionN, the canonical type given of anonymous functions. We will discuss this topic in the Functions are First Class and More Fun With Functions lectures. -
scala.PartialFunction; we will discuss this topic in the Partial Functions lecture of the Intermediate Scala course. -
Unit; discussed in the Learning Scala Using The REPL lecture. -
All types with literal notation:
Int,Long,Float,Double,Char,Boolean,String,Symbol(discussed in the Implicit Classes lecture of the Intermediate Scala course),java.lang.Class. - All numeric primitive types and
Chars. -
Option(discussed in the Option, Some and None lecture) and tuples (discussed in the Scala Tuples, Pattern Matching and Unapply lectures). -
java.lang.Throwable; discussed in the Try and try/catch/finally lecture. scala.Dynamic.scala.Singleton; discussed in the Self Types vs Inheritance lecture.- Most of
scala.reflect.*, espciallyClassTagandTypeTag. - Most of the annotations in
scala.annotation.*(e.g.,unchecked). scala.language.*; discussed in the Scala Imports and Packages lecture.
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.
-
scala.collection.Seq,NilandList(discussed in the Immutable Collections lecture of the Intermediate Scala course).WrappedArray, which is used for varargs parameters (discussed in the Classes Part 2 lecture).TupleNtypes; discussed in the Scala Tuples lectureProductandSerializable(for case classes, discussed in the Classes Part 1 and Classes Part 2 lectures).MatchError; discussed in the Pattern Matching lecture.scala.xml.*scala.DelayedInit; discussed in the More Fun With Functions lecture.
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) StringandStringBuilder(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) Serializablejava.rmi.Remoteandjava.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 ofAny, besidesAnyValandAnyRef. They have JavaScript semantics instead of Scala semantics. -
Stringand the boxed versions of all primitive types (heavily rewritten--so-called "hijacked"--by the compiler). -
js.ThisFunctionN: theirapplymethods behave differently than that of other JavaScript types (the first actual argument becomes thethisArgumentof the called function). js.UndefOrandjs.|(they behave as JS types even though they do not extendjs.Any).js.Object(new js.Object()is special-cased as an empty JS object literal{}).js.JavaScriptException(behaves very specially inthrowandcatch).js.WrappedArray(used by the desugaring of varargs).js.ConstructorTag(similar toClassTag).- The annotation
js.native, and all annotations injs.annotation.*
Plus, a dozen more primitive methods.
Scala Native-specific types
See NirDefinitions.scala.
- Unsigned integers:
UByte,UShort,UIntandULong. Ptr, the pointer type.FunctionPtrN, the function pointer types.- The annotations in
native.* - A number of extra primitive methods in
scala.scalanative.runtime.
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.