5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-26

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

下記のような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")
}
コンパイル・実行結果
-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)
}
コンパイル結果
[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. パターンガードあり・パターン漏れなし

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

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")
}
コンパイル・実行結果
200
bar
100

ケース4. パターンガードあり・パターン漏れあり

ここが本題です。

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

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)
}
コンパイル結果
(警告なしでコンパイルが成功する)

しかし、マッチしないパターンがきた場合は、ケース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)

(省略)

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

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")
}
コンパイル結果
(警告なしでコンパイルが成功する)
実行結果
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)
}
コンパイル結果
[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までお願いします。

  1. "-Xfatal-warnings"オプションのことです。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?