【Scala】sealed class/traitのサブクラスでパターンマッチ時、パターンガードを使うと網羅性チェックが行われない

知らずにハマったので備忘録です。

下記のような`sealed trait`とそのサブクラスで定義されたデータ型を考えます。

```Sample.scala
sealed trait Sample
case class Foo(n: Int) extends Sample
case object Bar extends Sample
```

このデータ型を使って、

- ケース1. パターンガードなし・パターン漏れなし
- ケース2. パターンガードなし・パターン漏れあり
- ケース3. パターンガードあり・パターン漏れなし
- ケース4. パターンガードあり・パターン漏れあり

の4ケースを見てみます。
なお、当記事のパターンマッチは全て部分関数の形式で書いていますが、match式の場合も結果は同様です。

#ケース1. パターンガードなし・パターン漏れなし
警告は出ず、実行時エラーにもなりません。

```Main.scala
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))

xs foreach  {
  case Foo(n) => println(n)
  case Bar => println("bar")
}
```

```powershell:コンパイル・実行結果
-200
bar
100
```

#ケース2. パターンガードなし・パターン漏れあり
Sampleトレイトは`sealed`のため、以下のようにパターンを網羅していない場合はコンパイル時に警告が出ます。

```Main.scala
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))

// Barのパターンを網羅していない
xs foreach  {
  case Foo(n) => println(n)
}
```

```powershell:コンパイル結果
[warn] C:xxx\src\main\scala\Main.scala:5:15: match may not be exhaustive.
[warn] It would fail on the following input: Bar
[warn]   xs foreach  {
[warn]               ^
[warn] one warning found
```

警告は出ますが、コンパイルエラーではないため実行は可能です。
ただし、マッチするパターンが存在しない場合は`MatchError`になります。

上記の例では`Seq`の1つ目の要素(`Foo(-200)`)は出力されますが。2つ目の要素(`Bar`)にマッチするパターンが存在しないため、その時点で実行時エラーになります。

```powershell:実行結果
-200
[error] (run-main-0) scala.MatchError: Bar (of class Bar$)
[error] scala.MatchError: Bar (of class Bar$)
[error]         at Main$.$anonfun$new$1(Main.scala:5)

(省略)
```

#ケース3. パターンガードあり・パターン漏れなし
ここからはパターンガードを絡めていきます。
パターンガードが入っている場合も、結局パターンに漏れがなければ問題はありません。
もちろん警告も出ません。

```Main.scala
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))

xs foreach  {
  case Foo(n) if n < 0 => println(-n) // 負数の場合は符号を反転して出力
  case Foo(n) => println(n)
  case Bar => println("bar")
}
```

```powershell:コンパイル・実行結果
200
bar
100
```

#ケース4. パターンガードあり・パターン漏れあり
###ここが本題です。

パターンを網羅していない場合かつパターンガードがある場合、<b>警告は出ません。</b>

```Main.scala
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))

// Barのパターンを網羅していない
xs foreach  {
  case Foo(n) if n < 0 => println(-n) // 負数の場合は符号を反転して出力
  case Foo(n) => println(n)
}
```

```powershell:コンパイル結果
(警告なしでコンパイルが成功する)
```

しかし、マッチしないパターンがきた場合は、ケース2の場合と同様`MatchError`になります。

```powershell:実行結果
200
[error] (run-main-3) scala.MatchError: Bar (of class Bar$)
[error] scala.MatchError: Bar (of class Bar$)
[error]         at Main$.$anonfun$new$1(Main.scala:4)

(省略)
```

また、下記のようにガードの結果通らないパターンが出来てしまう場合も同様です。

```Main.scala
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))

// n >= 0のパターンを網羅していない
xs foreach  {
  case Foo(n) if n < 0 => println(-n) // 負数の場合は符号を反転して出力
  case Bar => println("bar")
}
```

```powershell:コンパイル結果
(警告なしでコンパイルが成功する)
```

```powershell:実行結果
200
bar
[error] (run-main-4) scala.MatchError: Foo(100) (of class Foo)
[error] scala.MatchError: Foo(100) (of class Foo)
[error]         at Main$.$anonfun$new$1(Main.scala:4)

(省略)
```

##解決策
パターンガードを使わず、マッチ後にif式で分岐するようにします。
こうすればケース2と同様、コンパイル時に警告が出るようになります。

```Main.scala
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))

// Barのパターンを網羅していない
xs foreach  {
  case Foo(n) => if (n < 0) println(-n) else println(n)
}
```

```powershell:コンパイル結果
[warn] C:xxx\src\main\scala\Main.scala:4:15: match may not be exhaustive.
[warn] It would fail on the following input: Bar
[warn]   xs foreach  {
[warn]               ^
[warn] one warning found
```

#感想
`sealed class/trait`はパターンマッチ時の網羅性チェックがウリだと思っていたので、パターンガードを使用しただけで警告が出なくなるとは思いませんでした。

「単なる警告で何を大げさな」と思うかもしれません。
ですがScalaの場合、scalacのオプションで「警告をエラーにする」ことも可能です。[^1]
そういった人々にとっては、これは「コンパイルエラーになるはずのものがならなかった」と同義です。

せっかく`sealed`キーワードを用いることで網羅性に注意を割かなくてよくなったのに、今度はまた別の部分(パターンガードを使っているか、拾えていない条件はないか)に注意を割かなければいけなくなるというのは、技術的に仕方のないことかもしれませんが、少し残念に思ってしまいました。
<br>
最後までお読み頂きありがとうございました｡
質問や不備についてはコメント欄か[Twitter](https://twitter.com/ka2_kamaboko)までお願いします。

[^1]: "-Xfatal-warnings"オプションのことです。