Skip to content

Add code tabs for _overviews/scala3-book/types-union.md #2654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions _overviews/scala3-book/types-union.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ num: 51
previous-page: types-intersection
next-page: types-adts-gadts
---
<span class="tag tag-inline">Scala 3 only</span>

Used on types, the `|` operator creates a so-called _union type_.
The type `A | B` represents values that are **either** of the type `A` **or** of the type `B`.

In the following example, the `help` method accepts a parameter named `id` of the union type `Username | Password`, that can be either a `Username` or a `Password`:

{% tabs union-user %}

{% tab 'Scala 3 Only' %}

```scala
case class Username(name: String)
case class Password(hash: Hash)
Expand All @@ -23,59 +28,109 @@ def help(id: Username | Password) =
case Password(hash) => lookupPassword(hash)
// more code here ...
```
{% endtab %}

{% endtabs %}

We implement the method `help` by distinguishing between the two alternatives using pattern matching.

This code is a flexible and type-safe solution.
If you attempt to pass in a type other than a `Username` or `Password`, the compiler flags it as an error:

{% tabs union-error-1 %}

{% tab 'Scala 3 Only' %}

```scala
help("hi") // error: Found: ("hi" : String)
// Required: Username | Password
```

{% endtab %}

{% endtabs %}

You’ll also get an error if you attempt to add a `case` to the `match` expression that doesn’t match the `Username` or `Password` types:

{% tabs union-error-2 %}

{% tab 'Scala 3 Only' %}

```scala
case 1.0 => ??? // ERROR: this line won’t compile
```

{% endtab %}

{% endtabs %}

### Alternative to Union Types
As shown, union types can be used to represent alternatives of several different types, without requiring those types to be part of a custom-crafted class hierarchy, or requiring explicit wrapping.

#### Pre-planning the Class Hierarchy
Other languages would require pre-planning of the class hierarchy, like the following example illustrates:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Other languages would require pre-planning of the class hierarchy, like the following example illustrates:
In other languages, such as Scala 2, you would require pre-planning of the class hierarchy, like the following example illustrates:


{% tabs union-preplanning %}

{% tab 'Scala 3 Only' %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{% tab 'Scala 3 Only' %}
{% tab 'Scala 2 and 3' %}


```scala
trait UsernameOrPassword
case class Username(name: String) extends UsernameOrPassword
case class Password(hash: Hash) extends UsernameOrPassword
def help(id: UsernameOrPassword) = ...
```

{% endtab %}

{% endtabs %}

Pre-planning does not scale very well since, for example, requirements of API users might not be foreseeable.
Additionally, cluttering the type hierarchy with marker traits like `UsernameOrPassword` also makes the code more difficult to read.

#### Tagged Unions
Another alternative is to define a separate enumeration type like:

{% tabs union-tagged-unions %}

{% tab 'Scala 3 Only' %}

```scala
enum UsernameOrPassword:
case IsUsername(u: Username)
case IsPassword(p: Password)
```

{% endtab %}

{% endtabs %}

The enumeration `UsernameOrPassword` represents a _tagged_ union of `Username` and `Password`.
However, this way of modeling the union requires _explicit wrapping and unwrapping_ and, for instance, `Username` is **not** a subtype of `UsernameOrPassword`.

### Inference of Union Types
The compiler assigns a union type to an expression _only if_ such a type is explicitly given.
For instance, given these values:

{% tabs union-tagged-unions-example %}

{% tab 'Scala 3 Only' %}

```scala
val name = Username("Eve") // name: Username = Username(Eve)
val password = Password(123) // password: Password = Password(123)
```

{% endtab %}

{% endtabs %}

This REPL example shows how a union type can be used when binding a variable to the result of an `if`/`else` expression:

{% tabs union-tagged-unions-example-if %}

{% tab 'Scala 3 Only' %}

````
scala> val a = if true then name else password
val a: Object = Username(Eve)
Expand All @@ -84,6 +139,10 @@ scala> val b: Password | Username = if true then name else password
val b: Password | Username = Username(Eve)
````

{% endtab %}

{% endtabs %}

The type of `a` is `Object`, which is a supertype of `Username` and `Password`, but not the *least* supertype, `Password | Username`.
If you want the least supertype you have to give it explicitly, as is done for `b`.

Expand Down