Scala初心者必見!Option型の本質と基本操作

March 20, 2023

Tags /

Option 型のポイント

Option 型の本質

プログラミング中に Option 型に遭遇した場合、次の 2 つの解釈ができます。1 つ目は、ある値が存在するかどうかを表現したものです。これは単純に、値が存在する場合は Some となり、値が存在しない場合は None となります。2 つ目は、例外ハンドリング結果を表現したものです。正常に実行できた場合は Some となり、プログラムの実行中に意図した例外が発生した場合には None となります。

Option 型を抽象化した概念の 1 つに、エフェクトを閉じ込めることができるエフェクトフルコンテキストがあります。エフェクトとは、例外などの副作用で関数の純粋性を欠く要素のことです。エフェクトフルコンテキストを導入することで、関数を純粋なものにすることが可能になります。ここで、例外は純粋性を損なうエフェクトであり、Option 型はそれを吸収する役割を担っています。つまり、Option 型の本質は、例外というエフェクトを吸収し純粋関数を作成するための手段であることです。

単一の Option 型の値に対して処理をしたいとき

初心者がやりがちなのは、Option 型の値を取り出そうとすることですが、これは誤った発想です。代わりに、Option 型の値を変換する発想が必要です。Option 型の値を変換するには、map コンビネーターを使用することで実現できます。また、他にも Option 型から値を取り出さずに処理を行うコンビネーターが提供されていますので、こちらを使用することをおすすめします。ただし最終的にはどこかのタイミングで Option 型から値を取り出す必要がありますが、可能な限りそのタイミングは遅らせるべきです。

複数の Option 型の値に対して処理をしたいとき

初心者が陥りやすいのは、2 つの Option 型に対して処理をした結果、戻りの型が Option[Option[T]] のようにネストしてしまうことです。しかし、このネストしたものが表現するパターンは、以下のとおりです。

外側の Option 内側の Option 実質的に値があるかどうか
Some Some ある(Some)
Some None ない(None)
None - ない(None)

また、Option 型には別の解釈方法もあり、最大要素数が 1 のコレクションとして捉えることができます。この場合、Option[Option[T]]の全体の要素数は以下のとおりです。

外側の要素数 内側の要素数 全体の要素数
1 1 1(Some)
1 0 0(None)
0 - 0(None)

つまり、実質的には Some か None の 2 通りしかありませんので、Option[Option[T]]Option[T] と平坦化することが望ましいです。この平坦化を実現するには、flatMap コンビネーターを使用することで簡単に実現できます。

実務的には、まずは map コンビネーターを使って処理をしてみて、戻り値が Option[Option[T]] のようになってしまった場合は、外側の map を flatMap に変更することで解決できます。

このように Option[Option[T]]Option[T] に変換できる性質を含めた抽象化された概念をモナドと呼びます。つまり、flatMap とはモナドという概念を表していることになります。

for 式と flatMap の関係

初心者が誤解しやすいのは、Scala の for 式をループ処理の指定だと勘違いしていることです。しかし、Scala の for 式においてはその発想をまず捨ててください。for 式の本質は flatMap の書き換えであり、ループ処理ではありません。flatMap の階層が深くなってきた場合には、for 式に書き換えることをおすすめします。

以下に Option 型に対して for 式を適用した具体例を示します。

val iOpt = Some(1)
val jOpt = Some(2)
val kOpt = Some(3)

// flatMapで平坦化
val a = {
  iOpt flatMap { i =>
    jOpt flatMap { j =>
      kOpt map { k =>
        val sum = i + j + k
        sum
      }
    }
  }
}

// for式で平坦化
val b = for {
  i <- iOpt // #flatMap
  j <- jOpt // #flatMap
  k <- kOpt // #map
  sum = i + j + k
} yield {
  sum
}

// flatMapで平坦化した a と for式で平坦化した b は全く同じ
println(a) // Some(6)
println(b) // Some(6)

Option 型に対してパターンマッチングを使うべきか

原則として、Option 型に対してパターンマッチングを使用するべきではありませんが、柔軟な条件分岐が要求される場合は例外となります。Option 型には様々なコンビネータが用意されており、計算結果の合成が容易で、チェーン操作により直感的な実装が可能であり、可読性が向上します。一方で、簡単なシチュエーションでパターンマッチングを使用すると、冗長なコードになり可読性が低下することがあります。

以下は、典型的な悪いコード例です。

val opt: Option[Int] = Some(2)

opt match {
  case Some(value) => Some(value * 2)
  case None => None
}

関数シグネチャとして Option 型を使うべきか

関数シグネチャに Option 型は存在しない方が望ましいです。つまり、関数への入力値は必ず存在し、関数から返す値も必ず存在することが望ましいということです。ただし、実際にはプログラミングにおいて、一度でも Option 型に関わった場合、Option 型が付きまとうことがあります。そのため、関数の出力に Option 型が存在するのは一般的で、これは意図した例外が発生する可能性があることを示唆しています。一方、関数の入力に Option 型が存在する場合は、設計が適切でない可能性が高いです。

おわりに

本記事では、Option 型について説明しましたが、抽象化された概念は他の型(Future 型など)にも適用できるため、これらの型の役割や取り扱い方法を理解するためにも重要です。


Written by Gayamasan who lives and works in Japan.