前置き
Scalaには Option
型があります。これは値が存在するか否かをそのサブタイプである Some
と None
をつかい明示的に扱うことができます。
また、Option
型にはSome
であれば処理をし、None
ならなにもしないというようなメソッドが含まれるので、いちいちif文等を書く必要がありません。
def double(x: Option[Int]): Option[Int] = x.map(_ * 2)
double(Some(10)) // => Some(20)
double(None) // => None
値があったりなかったりする場面で便利に利用することができます。ただし、あらゆるケースで使おうとするのはやめてください。
ここからはタイトルにあるように推奨されない使用方法について述べようと思います。
常に値がある、または常に値がない場合
val foo: Option[Int] = Some(42)
val bar: Option[Int] = None
当然ですが、値があったりなかったりするときのものなので、そもそも必ず値がある場面では避けるべきです。
実際にもろにそのようなコードを書いてしまうことはあまりないですが、例えば次のようなコードはありがちです。
def f(a: Int, b: Option[Int]): Int
val foo: Option[Int] = Some(42)
f(13, foo)
関数 f
の引数型にあわせるために変数foo
をOption
型で定義しています。さらにfoo
が実際はSome
型なのをいいことに、get
を使い始めたら要注意です。
Some
とNone
の組み合わせが限定される場合
case class Foo private(a: Some[Int], b: Some[Int])
object Foo {
def makeA(a: Int): Foo = new Foo(Some(a), None)
def makeB(b: Int): Foo = new Foo(None, Some(b))
}
この例では他の箇所では直接コンストラクターは呼ばれないものとします。
Foo
のフィールドa
とb
はどちから一方のみが必ずSome
であり、他方はNone
となります。しかしながら、型としては両方Some
、両方None
がありえます。これはmatch式を書いた場合などに顕著な問題です。
foo match {
case Foo(Some(a), None) => // ...
case Foo(None, Some(b)) => // ...
case _ => // 実際は到達しないが警告になるので定義する
}
コメントの通り、実際には到達しないにもかかわらず警告に対応するためだけにケースを定義する必要があります。
対処としては有効な組み合わせをサブタイプにできるか検討しましょう。
sealed abstract class Foo
object Foo {
case class A(value: Int) extends Foo
case class B(value: Int) extends Foo
}
sealed
で定義することでコンパイラ(とコードを読む人間)に対してサブタイプがこのソースファイル内にしかないことを宣言できます。
サブタイプが限られているという定義はOption
などと同様です。Option
はこのsealedを使ったテクニックの1つをジェネリクスとして用意した型といえます。使用方法が完全にマッチしないなら直接sealed
で定義することを推奨します。
また、この例のように2つの値のどちらか一方が有効な場合を表現する汎用的な型として Either
があるので、こちらも検討してください。
None
では不十分な場合
たとえば Some
が 成功、 None
が 失敗 を表すという用途はよくあります。実際Scala標準ライブラリのコレクションではfind
は見つかればSome
、見つからなければNone
を返します。find
であれば見つからないこと以上の情報を求めていないのでNone
でよいですが、たとえばユーザー入力を受け取って処理を行う場合はどうでしょうか? ユーザーに意味のあるエラーメッセージを出したいなら失敗となった理由も返すべきです。
もし失敗した理由を返したいなら Try
を利用できます。
Try[A]
は成功した場合は A
型の値を保持した Success
型に、失敗した場合は Throwable
型の値を保持した Failure
型になります。
Trhowable
でなく任意の型の値を返したい場合は Eihter
を使用します。
Option
が多重になった場合
これは絶対に避けろとは言いませんが、避けた方が無難な例です。
def readContent(path: String): Option[String] = // 省略
def parseString(content: String): Option[Result] = // 省略
def parseFile(path: String): Option[Option[Result]] =
readContent.map(parseString)
このコードではファイルを開き、そのストリームを読み取って結果を返します。
ストリームの読み取り処理では結果がNoneになることがあります。
ファイルが開けなかった時と、読み取り処理でNoneになったときでNoneになる位置が違うので区別できますが、この値を利用する側からすると中途半端な情報です。どちらがNoneだったのかで呼び出し元で対応が必要なのであれば、Exceptionを返して欲しいですし、特に気にする必要がないのであれば多重ではないOption
であってほしいです。
つまり、Try[Result]
か単純なOption[Result]
にして欲しいところです。
まとめ
いくつか例を挙げましたが、これらに限らずより適切な型が存在することはあります。とても便利なOption
型で表現できたとしても、もっと正確な表現ができる適切な型がないか一度検討する価値はあります。