Syntactically, unions follow the same rules as intersections, but have a lower precedence, see Intersection Types - More Details.
Interaction with pattern matching syntax|
is also used in pattern matching to separate pattern alternatives and has lower precedence than :
as used in typed patterns, this means that:
is still equivalent to:
and not to:
Subtyping RulesA
is always a subtype of A | B
for all A
, B
.
If A <: C
and B <: C
then A | B <: C
Like &
, |
is commutative and associative:
A | B =:= B | A
A | (B | C) =:= (A | B) | C
&
is distributive over |
:
A & (B | C) =:= A & B | A & C
From these rules it follows that the least upper bound (LUB) of a set of types is the union of these types. This replaces the definition of least upper bound in the Scala 2 specification.
MotivationThe primary reason for introducing union types in Scala is that they allow us to guarantee that for every set of types, we can always form a finite LUB. This is both useful in practice (infinite LUBs in Scala 2 were approximated in an ad-hoc way, resulting in imprecise and sometimes incredibly long types) and in theory (the type system of Scala 3 is based on the DOT calculus, which has union types).
Additionally, union types are a useful construct when trying to give types to existing dynamically typed APIs, this is why they're an integral part of TypeScript and have even been partially implemented in Scala.js.
Join of a union typeIn some situation described below, a union type might need to be widened to a non-union type, for this purpose we define the join of a union type T1 | ... | Tn
as the smallest intersection type of base class instances of T1
,...,Tn
. Note that union types might still appear as type arguments in the resulting type, this guarantees that the join is always finite.
The visible join of a union type is its join where all operands of the intersection that are instances of transparent traits or classes are removed.
ExampleGiven
trait C[+T]
trait D
trait E
transparent trait X
class A extends C[A], D, X
class B extends C[B], D, E, X
The join of A | B
is C[A | B] & D & X
and the visible join of A | B
is C[A | B] & D
.
We distinguish between hard and soft union types. A hard union type is a union type that's explicitly written in the source. For instance, in
val x: Int | String = ...
Int | String
would be a hard union type. A soft union type is a type that arises from type checking an alternative of expressions. For instance, the type of the expression
val x = 1
val y = "abc"
if cond then x else y
is the soft unon type Int | String
. Similarly for match expressions. The type of
x match
case 1 => x
case 2 => "abc"
case 3 => List(1, 2, 3)
is the soft union type Int | "abc" | List[Int]
.
When inferring the result type of a definition (val
, var
, or def
) and the type we are about to infer is a soft union type, then we replace it by its visible join, provided it is not empty. Similarly, when instantiating a type argument, if the corresponding type parameter is not upper-bounded by a union type and the type we are about to instantiate is a soft union type, we replace it by its visible join, provided it is not empty. This mirrors the treatment of singleton types which are also widened to their underlying type unless explicitly specified. The motivation is the same: inferring types which are "too precise" can lead to unintuitive typechecking issues later on.
import scala.collection.mutable.ListBuffer
val x = ListBuffer(Right("foo"), Left(0))
val y: ListBuffer[Either[Int, String]] = x
This code typechecks because the inferred type argument to ListBuffer
in the right-hand side of x
was Left[Int, Nothing] | Right[Nothing, String]
which was widened to Either[Int, String]
. If the compiler hadn't done this widening, the last line wouldn't typecheck because ListBuffer
is invariant in its argument.
The members of a union type are the members of its join.
ExampleThe following code does not typecheck, because method hello
is not a member of AnyRef
which is the join of A | B
.
trait A { def hello: String }
trait B { def hello: String }
def test(x: A | B) = x.hello // error: value `hello` is not a member of A | B
On the other hand, the following would be allowed
trait C { def hello: String }
trait A extends C with D
trait B extends C with E
def test(x: A | B) = x.hello // ok as `hello` is a member of the join of A | B which is C
Exhaustivity checking
If the selector of a pattern match is a union type, the match is considered exhaustive if all parts of the union are covered.
ErasureThe erased type for A | B
is the erased least upper bound of the erased types of A
and B
. Quoting from the documentation of TypeErasure#erasedLub
, the erased LUB is computed as follows:
Object
Object
|A|
there are no minimal common superclasses or traits that come after S. The reason to pick last is that we prefer classes over traits that way, which leads to more predictable bytecode and (?) faster dynamic dispatch.RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4