Skip to content

Improve exhaustivity section of pattern matching #2463

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

Merged
merged 1 commit into from
Jul 11, 2022
Merged
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
64 changes: 45 additions & 19 deletions _tour/pattern-matching.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ println(showNotification(someVoiceRecording)) // prints You received a Voice Re
The function `showNotification` takes as a parameter the abstract type `Notification` and matches on the type of `Notification` (i.e. it figures out whether it's an `Email`, `SMS`, or `VoiceRecording`). In the `case Email(sender, title, _)` the fields `sender` and `title` are used in the return value but the `body` field is ignored with `_`.

## Pattern guards
Pattern guards are simply boolean expressions which are used to make cases more specific. Just add `if <boolean expression>` after the pattern.
Pattern guards are boolean expressions which are used to make cases more specific. Just add `if <boolean expression>` after the pattern.

{% tabs pattern-matching-5 class=tabs-scala-version %}
{% tab 'Scala 2' for=pattern-matching-5 %}
Expand Down Expand Up @@ -231,36 +231,62 @@ def goIdle(device: Device): String = device match

`def goIdle` has a different behavior depending on the type of `Device`. This is useful when the case needs to call a method on the pattern. It is a convention to use the first letter of the type as the case identifier (`p` and `c` in this case).

## Sealed classes
Traits and classes can be marked `sealed` which means all subtypes must be declared in the same file. This assures that all subtypes are known.
## Sealed types

You may have noticed that in the examples above the base types are qualified
with the keyword `sealed`. This provides extra safety because the compiler
checks that the `cases` of a `match` expression are exhaustive when the base
type is `sealed`.

For instance, in the method `showNotification` defined above, if we forget
one case, say, `VoiceRecording`, the compiler emits a warning:

{% tabs pattern-matching-7 class=tabs-scala-version %}
{% tab 'Scala 2' for=pattern-matching-7 %}
```scala mdoc
sealed trait Furniture
case class Couch(descriptor: String) extends Furniture
case class Chair(descriptor: String) extends Furniture

def findPlaceToSit(piece: Furniture): String = piece match {
case Couch(descriptor) => s"Lie on the $descriptor couch"
case Chair(descriptor) => s"Sit on the $descriptor chair"
```scala
def showNotification(notification: Notification): String = {
notification match {
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
}
}
```
{% endtab %}
{% tab 'Scala 3' for=pattern-matching-7 %}
```scala
sealed trait Furniture
case class Couch(descriptor: String) extends Furniture
case class Chair(descriptor: String) extends Furniture

def findPlaceToSit(piece: Furniture): String = piece match
case Couch(descriptor) => s"Lie on the $descriptor couch"
case Chair(descriptor) => s"Sit on the $descriptor chair"
def showNotification(notification: Notification): String =
notification match
case Email(sender, title, _) =>
s"You got an email from $sender with title: $title"
case SMS(number, message) =>
s"You got an SMS from $number! Message: $message"
```
{% endtab %}
{% endtabs %}

This is useful for pattern matching because we don't need a "catch all" case.
This definition produces the following warning:

~~~
match may not be exhaustive.

It would fail on pattern case: VoiceRecording(_, _)
~~~

The compiler even provides examples of input that would fail!

On the flip side, exhaustivity checking requires you to define all the subtypes
of the base type in the same file as the base type (otherwise, the compiler
would not know what are all the possible cases). For instance, if you try
to define a new type of `Notification` outside of the file that defines
the `sealed trait Notfication`, it will produce a compilation error:

~~~
case class Telepathy(message: String) extends Notification
^
Cannot extend sealed trait Notification in a different source file
~~~

## Notes

Expand Down