1. 概要

このチュートリアルでは、Scalaでオプションのデータ要素を管理する方法を学びます。 ScalaのOptionタイプを使用すると、アプリケーションコード全体で意図したとおりに使用すれば、堅牢なコードを簡単に記述できます。

2. オプション基本

オプションの値を管理および表現するには、慎重に検討する必要があります。 私たち全員が経験したように、データの欠如を処理するコードを書くことは書くのが難しく、多くの実行時エラーの原因になります。

ScalaのOptionは、2つの自己強化的な方法でオプション値の管理を可能にするため、特に便利です。

  1. 型安全性–オプションの値をパラメーター化できます。
  2. 機能認識– Option タイプは、バグの作成を減らすのに役立つ一連の強力な機能機能も提供します。

さらに、 Option クラスを学習して習得することは、Scalaの機能パラダイムをコードに採用するための優れた方法です。 途中でそれらについて話します。

Scalaがオプションの値を表す方法から始めましょう:

           Option[T]
              ^
              |
      +-------+------+
      |              |
      |              |
    Some[T]        None[T]

基本クラスscala.Optionは抽象クラスであり、scala.collection.IterableOnceを拡張します。 これにより、ScalaのOptionはコンテナのように動作します。 これは、このチュートリアルの後半で Option を使用してマッピングとフィルタリングについて説明するときに、有効に活用できる機能です。

ここで重要なポイントは、 Option クラスとそのサブクラスはすべて、明示的または推測可能な具象型を必要とすることです。

val o1: Option[Int] = None
val o2 = Some(10)

o1とo2はどちらも、 Option[Int]のインスタンスです。

最後に、Optionが「null認識」であることを知っておくと便利です。

val o1: Option[Int] = Option(null)
assert(false == o1.isDefined)

これは、Java、特にnullを使用して「見つからない」を表す古いJavaライブラリと相互運用する場合に便利です。

2.1. オプション一部またはなしをテストします

次の方法を使用して、OptionSomeまたはNoneであるかどうかをテストできます。

  • isDefined –オブジェクトが Some の場合、 true
  • nonEmpty –オブジェクトが Some の場合、 true
  • isEmpty –オブジェクトが None の場合、 true

2.2. オプションのコンテンツを取得する

get メソッドを使用して、Optionの値を取得できます。 Noneのインスタンスでgetメソッドを呼び出すと、NoSuchElementExceptionがスローされます。この動作は「成功バイアス」と呼ばれます。 成功バイアスに慣れることは、Scalaの旅の重要なステップであり、Optionタイプはその旅を始めるのに最適な場所です。

このため、次のようなコードを記述したくなります。

val o1: Option[Int] = ...
val v1 = if (o1.isDefined) {
  o1.get
} else {
  0
}

または、そのパターンマッチングの対応物:

val o1: Option[Int] = ...
val v1 = o1 match {
  case Some(n) =>
    n
  case None =>
    0
}

これらは両方とも、機能しないコーディング構造に依存しているため、Scalaコミュニティではアンチパターンと見なされます。 他のOptionアンチパターンには、 if /elseまたはmatchを使用してOptionを別のOptionにマップすることが含まれます。 この記事の後半で説明する既存の方法を使用して同じ効果を実現するための、より慣用的で洗練された方法があります。

2.3. オプションデフォルト値

他にもいくつかの検索方法があります。

  • getOrElse –オブジェクトが Some の場合は値を取得し、それ以外の場合はデフォルト値を返します
  • orElse Someの場合はOption を取得し、それ以外の場合は代替のOptionを返します。

最初のメソッドgetOrElseは、オプションの値が設定されていない場合にデフォルト値を返したい状況で役立ちます。 前のセクションのコードスニペットをgetOrElseを使用するように書き直してみましょう。

val v1 = o1.getOrElse(0)

明らかに、これは保守が簡単で簡単です。 さらに、単に値を返すだけではありません。 これも同様に有効です。

val usdVsZarFxRate: Option[BigDecimal] = ... // populated via a web service call, for example
val marketRate = usdVsZarFxRate.getOrElse(throw new RuntimeException("No exchange rate defined for USD/ZAR"))

ここで、Webサービス呼び出しを介して取得した連邦為替レートがあると想像してください。 ただし、これらのレートを取得するには、ユーザーを有効にする必要があります。 それらが有効になっていない場合、WebサービスコードはNoneを返します。 これにより、後続の作業を実行できないため、このようなコードブロックをすばやく終了できます。

3. コンテナとしてのオプション

すでに説明したように、Optionは別の値のコンテナーです。 この点で、単一の値ではありますが、コレクションの特殊なケースとして扱うことができます。 したがって、リストをトラバースするのと同じ方法で、オプションをトラバースできます。 Scalaの関数型プログラミングのサポートと組み合わせることで、より簡潔でエラーの少ないコードを記述できます。

3.1. マッピングオプション

Option.mapメソッドの正式な定義は次のとおりです。

final def map[B](f: (A) => B): Option[B]

いくつかの例としてこれを開梱しましょう:

val o1: Option[Int] = Some(10)
assert(o1.map(_.toString).contains("10"))
assert(o1.map(_ * 2.0).contains(20))

val o2: Option[Int] = None
assert(o2.map(_.toString).isEmpty)

Option.map を使用して、含まれている値を別のタイプに変換できるのは当然のことです。

さらに、 Option は、Option内の複数のレイヤーを折りたたむために使用されるflatMapメソッドを提供します。

ゲームトーナメントを追跡するための単純な特性とクラスのセットを使用して、これらの概念を調べてみましょう。

trait Player {
  def name: String
  def getFavoriteTeam: Option[String]
}

trait Tournament {
  def getTopScore(team: String): Option[Int] // Specified team's top score or None if they haven't played yet
}

val player: Player = ...                     // Let's not worry how we instantiate these just yet
val tournament: Tournament = ...

プレーヤーのお気に入りのチームのトップスコアを取得するにはどうすればよいですか? player.getFavoriteTeam()。map(tournament.getTopScore)を呼び出すこともできますが、これは Option [Option[Int]]を返します。 確かに、Optionの複数のレイヤーでの作業は厄介です。

Option。map を使用する代わりに、別のメソッドOption.flatMapを使用できます。 Option の中間レイヤーを削除することで、まさに私たちが望むことを実行します。 getTopScoreの実装を更新して使用してみましょう。

def getTopScore(player: Player, tournament: Tournament): Option[(Player, Int)] = {
  player.getFavoriteTeam.flatMap(tournament.getTopScore).map(score => (player, score))
}

最後に、 Option の値にアクセスしたいだけの場合は、foreachメソッドを使用できます。 これは、値の使用を気にしない場合に役立ちます。 これは、値をデータベースに保存したり、ファイルに出力したりする場合など、「副作用」の状況でよく使用されます。

val o1: Option[Int] = Some(10)
o1.foreach(println)

これにより、o1の値がstdoutに出力されます。

3.2. オプションを使用したフロー制御

Option.mapを使用してフロー制御を実行することもできます。 次のことを考慮してください。

val o1: Option[Int] = Some(10)
val o2: Option[Int] = None

def times2(n: Int): Int = n * 2

assert(o1.map(times2).contains(20))
assert(o2.map(times2).isEmpty)

ここでは、 Option[Int]の2つのインスタンスを定義しました。 1つ目は値が10で、2つ目は空です。 次に、入力に2を掛けるだけの単純な関数を定義します。

最後に、 times2 関数を使用して、 Option o1、、およびo2のそれぞれをマップします。 当然のことながら、 o1 をマッピングするとSome(20)が得られ、o2をマッピングするとNoneが得られます。

ここで重要なポイントは、2番目の呼び出し o2.map(times2)では、times2関数がまったく呼び出されないことです。

Option によって提供される、フロー制御を実装できる他のメソッドを使用した、より複雑な例を考えてみましょう。

3.3. フィルタリングによるフロー制御

オプションでコレクションスタイルのフィルタリングを行うこともできます。

  • filter Optionの値がSomeで、指定された関数が true を返す場合、Optionを返します。
  • filterNot Optionの値がSomeで、指定された関数が false を返す場合、Optionを返します。
  • examples Option が設定されていて、提供された関数が true を返す場合、trueを返します。
  • forall オプションには最大1つの値があるため、が存在すると同じ動作

これらのメソッドを使用すると、非常に簡潔なコードを記述できます。 たとえば、2人のユーザーのお気に入りのチームのどちらが最高のスコアを持っているかを計算する必要がある状況を考えてみましょう。

 def whoHasTopScoringTeam(playerA: Player, playerB: Player, tournament: Tournament): Option[(Player, Int)] = {
    getTopScore(playerA, tournament).foldRight(getTopScore(playerB, tournament)) {
      case (playerAInfo, playerBInfo) => playerBInfo.filter {
        case (_, scoreB) => scoreB > playerAInfo._2
      }.orElse(Some(playerAInfo))
    }
  }

ここでは、Optionがコレクションのように動作できるという事実を活用しています。 ここでの違いは、Optionは最大で1つの値を持つことができるということです。

このようなコードは最初は不透明に見えるかもしれませんが、Scalaイディオム、特にそのモナディック演算子の能力に慣れてくると、使いやすくなります。 None を明示的にチェックする必要はなく、明示的な if /elseチェックを実行する必要もないことに注意してください。

4. 結論

この記事では、ScalaのOptionクラスの基本的な使用法について説明しました。 その基本的な知識を拡張して、Optionの機能インターフェイスを使用して簡潔で慣用的なScalaを作成する方法を示しました。

いつものように、完全なソースコードはGitHubにあります。