Skip to content

Commit b2f2db6

Browse files
committed
Update documentation of union types
- Introduce hard and soft unions - Explain how transparency of base traits influences type inference - Drop outdated note on possible future changes
1 parent 74c079f commit b2f2db6

File tree

3 files changed

+78
-22
lines changed

3 files changed

+78
-22
lines changed

docs/_docs/reference/new-types/union-types-spec.md

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ a non-union type, for this purpose we define the _join_ of a union type `T1 |
7272
`T1`,...,`Tn`. Note that union types might still appear as type arguments in the
7373
resulting type, this guarantees that the join is always finite.
7474

75+
The _visible join_ of a union type is its join where all operands of the intersection that
76+
are instances of [transparent](../other-new-features/transparent-traits.md) traits or classes are removed.
77+
78+
7579
### Example
7680

7781
Given
@@ -80,31 +84,50 @@ Given
8084
trait C[+T]
8185
trait D
8286
trait E
83-
class A extends C[A] with D
84-
class B extends C[B] with D with E
87+
transparent trait X
88+
class A extends C[A], D, X
89+
class B extends C[B], D, E, X
8590
```
8691

87-
The join of `A | B` is `C[A | B] & D`
92+
The join of `A | B` is `C[A | B] & D & X` and the visible join of `A | B` is `C[A | B] & D`.
93+
94+
## Hard and Soft Union Types
95+
96+
We distinguish between hard and soft union types. A _hard_ union type is a union type that's explicitly
97+
written in the source. For instance, in
98+
```scala
99+
val x: Int | String = ...
100+
```
101+
`Int | String` would be a hard union type. A _soft_ union type is a type that arises from type checking
102+
an alternative of expressions. For instance, the type of the expression
103+
```scala
104+
val x = 1
105+
val y = "abc"
106+
if cond then x else y
107+
```
108+
is the soft unon type `Int | String`. Similarly for match expressions. The type of
109+
```scala
110+
x match
111+
case 1 => x
112+
case 2 => "abc"
113+
case 3 => List(1, 2, 3)
114+
```
115+
is the soft union type `Int | "abc" | List[Int]`.
116+
88117

89118
## Type inference
90119

91120
When inferring the result type of a definition (`val`, `var`, or `def`) and the
92-
type we are about to infer is a union type, then we replace it by its join.
121+
type we are about to infer is a soft union type, then we replace it by its visible join,
122+
provided it is not empty.
93123
Similarly, when instantiating a type argument, if the corresponding type
94124
parameter is not upper-bounded by a union type and the type we are about to
95-
instantiate is a union type, we replace it by its join. This mirrors the
125+
instantiate is a soft union type, we replace it by its visible join, provided it is not empty.
126+
This mirrors the
96127
treatment of singleton types which are also widened to their underlying type
97128
unless explicitly specified. The motivation is the same: inferring types
98129
which are "too precise" can lead to unintuitive typechecking issues later on.
99130

100-
**Note:** Since this behavior limits the usability of union types, it might
101-
be changed in the future. For example by not widening unions that have been
102-
explicitly written down by the user and not inferred, or by not widening a type
103-
argument when the corresponding type parameter is covariant.
104-
105-
See [PR #2330](https://github.com/lampepfl/dotty/pull/2330) and
106-
[Issue #4867](https://github.com/lampepfl/dotty/issues/4867) for further discussions.
107-
108131
### Example
109132

110133
```scala

docs/_docs/reference/new-types/union-types.md

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ A union type `A | B` has as values all values of type `A` and also all values of
88

99

1010
```scala
11-
case class UserName(name: String)
12-
case class Password(hash: Hash)
11+
trait ID
12+
case class UserName(name: String) extends ID
13+
case class Password(hash: Hash) extends ID
1314

1415
def help(id: UserName | Password) =
1516
val user = id match
@@ -22,7 +23,10 @@ Union types are duals of intersection types. `|` is _commutative_:
2223
`A | B` is the same type as `B | A`.
2324

2425
The compiler will assign a union type to an expression only if such a
25-
type is explicitly given. This can be seen in the following [REPL](https://docs.scala-lang.org/overviews/repl/overview.html) transcript:
26+
type is explicitly given or the common supertype of all alternatives is [transparent](../other-new-features/transparent-traits.md).
27+
28+
29+
This can be seen in the following [REPL](https://docs.scala-lang.org/overviews/repl/overview.html) transcript:
2630

2731
```scala
2832
scala> val password = Password(123)
@@ -32,15 +36,36 @@ scala> val name = UserName("Eve")
3236
val name: UserName = UserName(Eve)
3337

3438
scala> if true then name else password
35-
val res2: Object = UserName(Eve)
39+
val res1: ID = UserName(Eve)
3640

3741
scala> val either: Password | UserName = if true then name else password
38-
val either: Password | UserName = UserName(Eve)
42+
val either: UserName | Password = UserName(Eve)
3943
```
40-
41-
The type of `res2` is `Object & Product`, which is a supertype of
42-
`UserName` and `Password`, but not the least supertype `Password |
43-
UserName`. If we want the least supertype, we have to give it
44+
The type of `res1` is `ID`, which is a supertype of
45+
`UserName` and `Password`, but not the least supertype `UserName | Password`.
46+
If we want the least supertype, we have to give it
4447
explicitly, as is done for the type of `either`.
4548

49+
The inference behavior changes if the common supertrait `ID` is declared `transparent`:
50+
```scala
51+
transparent trait ID
52+
```
53+
In that case the union type is not widened.
54+
```scala
55+
scala> if true then name else password
56+
val res2: UserName | Password = UserName(Eve)
57+
```
58+
The more precise union type is also inferred if `UserName` and `Password` are declared without an explicit
59+
parent, since in that case their implied superclass is `Object`, which is among the classes that are
60+
assumed to be transparent. See [Transparent Traits and Classes](../other-new-features/transparent-traits.md)
61+
for a list of such classes.
62+
```scala
63+
case class UserName(name: String)
64+
case class Password(hash: Hash)
65+
66+
scala> if true then UserName("Eve") else Password(123)
67+
val res3: UserName | Password = UserName(Eve)
68+
```
69+
70+
4671
[More details](./union-types-spec.md)

tests/pos/transparent-union.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
transparent trait ID
2+
case class UserName(name: String) extends ID
3+
case class Password(hash: Int) extends ID
4+
5+
val password: Password = Password(123)
6+
val name = UserName("Eve")
7+
val res = if ??? then name else password
8+
val _: UserName | Password = res

0 commit comments

Comments
 (0)