Skip to content

Commit 943c06a

Browse files
committed
Add detailed union types documentation
1 parent 66c97fc commit 943c06a

File tree

2 files changed

+158
-9
lines changed

2 files changed

+158
-9
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
---
2+
layout: doc-page
3+
title: "Union Types - More Details"
4+
---
5+
6+
## Syntax
7+
8+
Syntactically, unions follow the same rules as intersections, but have a lower precedence, see
9+
[Intersection Types - More Details](http://lampepfl.github.io/dotty/docs/reference/intersection-types-spec.html).
10+
11+
### Interaction with pattern matching syntax
12+
`|` is also used in pattern matching to separate pattern alternatives and has
13+
lower precedence than `:` as used in typed patterns, this means that:
14+
15+
``` scala
16+
case _: A | B => ...
17+
```
18+
is still equivalent to:
19+
``` scala
20+
case (_: A) | B => ...
21+
```
22+
and not to:
23+
``` scala
24+
case _: (A | B) => ...
25+
```
26+
27+
## Subtyping Rules
28+
29+
- `A` is always a subtype of `A | B` for all `A`, `B`.
30+
- If `A <: C` and `B <: C` then `A | B <: C`
31+
- Like `&`, `|` is commutative and associative:
32+
```scala
33+
A | B =:= B | A
34+
A | (B | C) =:= (A | B) | C
35+
```
36+
- `&` is distributive over `|`:
37+
```scala
38+
A & (B | C) =:= A & B | A & C
39+
```
40+
41+
From these rules it follows that the _least upper bound_ (lub) of a set of type
42+
is the union of these types. This replaces the
43+
[definition of least upper bound in the Scala 2 specification](https://www.scala-lang.org/files/archive/spec/2.12/03-types.html#least-upper-bounds-and-greatest-lower-bounds).
44+
45+
## Motivation
46+
47+
The primary reason for introducing union types in Scala is that they allow us to
48+
guarantee that for every set of type, we can always form a finite lub. This is
49+
both useful in practice (infinite lubs in Scala 2 were approximated in an ad-hoc
50+
way, resulting in imprecise and sometimes incredibly long types) and in theory
51+
(the type system of Scala 3 is based on the
52+
[DOT calculus](https://infoscience.epfl.ch/record/227176/files/soundness_oopsla16.pdf),
53+
which has union types).
54+
55+
Additionally, union types are a useful construct when trying to give types to existing
56+
dynamically typed APIs, this is why they're [an integral part of TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types)
57+
and have even been [partially implemented in Scala.js](https://github.com/scala-js/scala-js/blob/master/library/src/main/scala/scala/scalajs/js/Union.scala).
58+
59+
## Join of a union type
60+
61+
In some situation described below, a union type might need to be widened to
62+
a non-union type, for this purpose we define the _join_ of a union type `T1 |
63+
... | Tn` as the smallest intersection type of base class instances of
64+
`T1`,...,`Tn`. Note that union types might still appear as type arguments in the
65+
resulting type, this guarantees that the join is always finite.
66+
67+
### Example
68+
69+
Given
70+
71+
```scala
72+
trait C[+T]
73+
trait D
74+
class A extends C[A] with D
75+
class B extends C[B] with D with E
76+
```
77+
78+
The join of `A | B` is `C[A | B] with D`
79+
80+
## Type inference
81+
82+
When inferring the result type of a definition (`val`, `var`, or `def`), if the
83+
type we are about to infer is a union type, we replace it by its join.
84+
Similarly, when instantiating a type argument, if the corresponding type
85+
parameter is not upper-bounded by a union type and the type we are about to
86+
instantiate is a union type, we replace it by its join. This mirrors the
87+
treatment of singleton types which are also widened to their underlying type
88+
unless explicitly specified. and the motivation is the same: inferring types
89+
which are "too precise" can lead to unintuitive typechecking issues later on.
90+
91+
Note: Since this behavior severely limits the usability of union types, it might
92+
be changed in the future. For example by not widening unions that have been
93+
explicitly written down by the user and not inferred, or by not widening a type
94+
argument when the corresponding type parameter is covariant. See
95+
[#2330](https://github.com/lampepfl/dotty/pull/2330) and
96+
[#4867](https://github.com/lampepfl/dotty/issues/4867) for further discussions.
97+
98+
### Example
99+
100+
```scala
101+
import scala.collection.mutable.ListBuffer
102+
val x = ListBuffer(Right("foo"), Left(0))
103+
val y: ListBuffer[Either[Int, String]] = x
104+
```
105+
106+
This code typechecks because the inferred type argument to `ListBuffer` in the
107+
right-hand side of `x` was `Left[Int, Nothing] | Right[Nothing, String]` which
108+
was widened to `Either[Int, String]`. If the compiler hadn't done this widening,
109+
the last line wouldn't typecheck because `ListBuffer` is invariant in its
110+
argument.
111+
112+
113+
## Members
114+
115+
The members of a union type are the members of its join.
116+
117+
### Example
118+
119+
The following code does not typecheck, because `hello` is not a member of
120+
`AnyRef` which is the join of `A | B`.
121+
122+
```scala
123+
trait A { def hello: String }
124+
trait B { def hello: String }
125+
126+
def test(x: A | B) = x.hello // error: value `hello` is not a member of A | B
127+
```
128+
129+
## Exhaustivity checking
130+
131+
If the selector of a pattern match is a union type, the match is considered
132+
exhaustive if all parts of the union are covered.
133+
134+
## Erasure
135+
136+
The erased type for `A | B` is the _erased least upper bound_ of the erased
137+
types of `A` and `B`. Quoting from the documentation of `TypeErasure#erasedLub`,
138+
the erased lub is computed as follows:
139+
- if both argument are arrays of objects, an array of the erased lub of the element types
140+
- if both arguments are arrays of same primitives, an array of this primitive
141+
- if one argument is array of primitives and the other is array of objects, Object
142+
- if one argument is an array, Object
143+
- otherwise a common superclass or trait S of the argument classes, with the
144+
following two properties:
145+
* S is minimal: no other common superclass or trait derives from S
146+
* S is last : in the linearization of the first argument type `|A|`
147+
there are no minimal common superclasses or traits that
148+
come after S.
149+
The reason to pick last is that we prefer classes over traits that way,
150+
which leads to more predictable bytecode and (?) faster dynamic dispatch.
151+
152+
## Limitations
153+
154+
In a union type `A | B`, neither `A` nor `B` is allowed to be a singleton type.
155+
This is an implementation restriction that may be lifted in the future.

docs/docs/reference/union-types.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,7 @@ val either: Password | UserName = UserName(Eve)
4444
The type of `res2` is `Object & Product`, which is a supertype of
4545
`UserName` and `Product`, but not the least supertype `Password |
4646
UserName`. If we want the least supertype, we have to give it
47-
explicitly, as is done for the type of `either`. More precisely, the
48-
typechecker will _widen_ a union type to a non-union type when
49-
inferring the type of `val` or `var`, or the result type of a `def`,
50-
or the argument to pass for a type parameter. The widened type of `A
51-
| B` is usually the intersection of all class or trait types that are
52-
supertypes of both `A` and `B`; it does not include any refinements.
53-
Union types are in that sense analogous to singleton types `x.type`
54-
which are also widened to their underlying type unless explicitly
55-
specified.
47+
explicitly, as is done for the type of `either`.
48+
49+
[More details](./union-types-spec.html)
5650

0 commit comments

Comments
 (0)