0
1

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でOptionを使うことを推奨しないケース

Posted at

前置き

Scalaには Option 型があります。これは値が存在するか否かをそのサブタイプである SomeNone をつかい明示的に扱うことができます。
また、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 の引数型にあわせるために変数fooOption型で定義しています。さらにfooが実際はSome型なのをいいことに、getを使い始めたら要注意です。

SomeNoneの組み合わせが限定される場合

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のフィールドabはどちから一方のみが必ず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型で表現できたとしても、もっと正確な表現ができる適切な型がないか一度検討する価値はあります。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?