Tips 2:sealed修飾子でパターンの漏れを自動検出する

Scalaでのプログラミングに慣れてくると、case classとそれに対するパターンマッチングを多用するようになってきます。たとえば、ScalaのOptionのような型に、エラー時の情報を持たせられるようにした、InformativeOptionというクラスを定義したいとします。素直に書くと次のようになるでしょう。

abstract trait InformativeOption[+A] case class Success[+A](value: A) extends InformativeOption[A] case class Failure(reason: Exception) extends InformativeOption[Nothing]

このコードをInformativeOption.scalaというファイルに保存しておき、scalacで事前にコンパイルしておきます。

このInformativeOption は次のようにして使えます。

import java.io._ object UsingInformativeOption extends App { def openForInputStream(file: File): InformativeOption[InputStream] = try { Success(new FileInputStream(file)) } catch { case e: FileNotFoundException => Failure(e) } openForInputStream(new File("aaa.txt")) match { case Success(stream) => println("open succeed") case Failure(e) => println("open failed because the following exception:" + e) } }

このコードをscalacでコンパイルして(ファイル名はUsingInformativeOption.scalaとします)、実行すると、次のように表示されます。

> scala UsingInformativeOption open failed because the following exception:java.io.FileNotFoundException: aaa.txt (指定されたファイルが見つかりません。 )

さて、これで、失敗した場合の例外情報を持ち運べる型を無事定義できたことがわかりました。しかし、次のように片方のパターンを書き忘れたらどうなるでしょうか?

openForInputStream(new File("aaa.txt")) match { case Success(stream) => println("open succeed") }

scala.MatchError: Failure(java.io.FileNotFoundException: aaa.txt (指定されたファイルが見つかりません。)) (of class Failu re)

大変残念なことに実行時のMatchErrorが出てしまいます。この場合、パターンが二つだけですから書き損じの危険性は比較的低いですが、もっとパターン数が増えてくるとミスの可能性は上がっていきます。この問題を回避する方法は無いのでしょうか?「あります。それがsealed修飾子です」、というのがこの話の本題です。

さて、sealed修飾子を付けてInformativeOptionを書き直してみましょう。

abstract sealed trait InformativeOption[+A] case class Success[+A](value: A) extends InformativeOption[A] case class Failure(reason: Exception) extends InformativeOption[Nothing]

変更箇所は一箇所だけで、InformativeOptionクラスの定義にsealedを付けただけです。このコードを先ほどと同じようにscalacでコンパイルした上で、先ほどの、片方のパターンを書き忘れたUsingInformativeOptionクラスをコンパイルしてみましょう。すると、次のような警告がコンパイル時に表示されます。

UsingInformativeOption.scala:8: warning: match is not exhaustive! missing combination Failure openForInputStream(new File("aaa.txt")) match { ^ one warning found

このエラーメッセージは、パターンマッチが網羅的(exhaustive)でない、つまり、パターンの記述漏れがあるということを表しています。

このようにして、パターンマッチに使うクラスやトレイトにはsealedを付加しておくことで、パターンの記述漏れをコンパイル時に検出できます(ある程度の制限はありますが)。sealedによって、くだらないケアレスミスで時間を浪費する事を防ぐことができますから、パターンマッチングに使うデータ構造のルートクラスにはsealedを付加するのがデフォルトだと考えるのが良いでしょう。