知らずにハマったので備忘録です。
下記のようなsealed trait
とそのサブクラスで定義されたデータ型を考えます。
sealed trait Sample
case class Foo(n: Int) extends Sample
case object Bar extends Sample
このデータ型を使って、
- ケース1. パターンガードなし・パターン漏れなし
- ケース2. パターンガードなし・パターン漏れあり
- ケース3. パターンガードあり・パターン漏れなし
- ケース4. パターンガードあり・パターン漏れあり
の4ケースを見てみます。
なお、当記事のパターンマッチは全て部分関数の形式で書いていますが、match式の場合も結果は同様です。
ケース1. パターンガードなし・パターン漏れなし
警告は出ず、実行時エラーにもなりません。
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))
xs foreach {
case Foo(n) => println(n)
case Bar => println("bar")
}
-200
bar
100
ケース2. パターンガードなし・パターン漏れあり
Sampleトレイトはsealed
のため、以下のようにパターンを網羅していない場合はコンパイル時に警告が出ます。
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))
// Barのパターンを網羅していない
xs foreach {
case Foo(n) => println(n)
}
[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
)にマッチするパターンが存在しないため、その時点で実行時エラーになります。
-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. パターンガードあり・パターン漏れなし
ここからはパターンガードを絡めていきます。
パターンガードが入っている場合も、結局パターンに漏れがなければ問題はありません。
もちろん警告も出ません。
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")
}
200
bar
100
ケース4. パターンガードあり・パターン漏れあり
ここが本題です。
パターンを網羅していない場合かつパターンガードがある場合、警告は出ません。
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)
}
(警告なしでコンパイルが成功する)
しかし、マッチしないパターンがきた場合は、ケース2の場合と同様MatchError
になります。
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)
(省略)
また、下記のようにガードの結果通らないパターンが出来てしまう場合も同様です。
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")
}
(警告なしでコンパイルが成功する)
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と同様、コンパイル時に警告が出るようになります。
val xs: Seq[Sample] = Seq(Foo(-200), Bar, Foo(100))
// Barのパターンを網羅していない
xs foreach {
case Foo(n) => if (n < 0) println(-n) else println(n)
}
[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
キーワードを用いることで網羅性に注意を割かなくてよくなったのに、今度はまた別の部分(パターンガードを使っているか、拾えていない条件はないか)に注意を割かなければいけなくなるというのは、技術的に仕方のないことかもしれませんが、少し残念に思ってしまいました。
最後までお読み頂きありがとうございました。
質問や不備についてはコメント欄かTwitterまでお願いします。
-
"-Xfatal-warnings"オプションのことです。 ↩