1. 概要

命令型プログラミング言語では、for-loopwhile-loopなどのloopsを使用してコレクションを反復処理します。 Scalaプログラミング言語は、新しい種類のループ、for-comprehensionを導入しました。

他の多くのScalaコンストラクトと同様に、for-comprehensionはHaskellから直接提供されます。 その使用は、単にコレクションをループするだけではなく、プログラミングに機能的なアプローチを使用するときに構文の複雑さに対処するのに役立ちます。

このチュートリアルでは、Scalaのfor-comprehension構造について深く掘り下げます。

2. 従来型vs. Javaでの宣言型ループ

Javaでのループの簡単なツアーに参加して、Scalaのfor-comprehensionの背後にある動機を見てみましょう。

2.1. Java8より前

Scalaでは、ループはあまり好きではありません。 実際、コレクションに対するいくつかの副作用を含むScala while-loopまたはfor-loopを見つけるのは困難です。

ただし、 Javaでは、のようなコードを見つけることは珍しくありません。

final List<TestResult> results =
  Arrays.asList(new TestResult("test 1",10, 10), new TestResult("test 2",2, 6));
int totalPassedAssertsInSucceededTests = 0;
for (int i = 0; i < results.size(); i++) {
    final TestResult result = results.get(i);
    if (result.isSucceeded()) {
        totalPassedAssertsInSucceededTests += result.getSuccessfulAsserts();
    }
}

Scalaでは、このスタイルのプログラミングは、コレクションをループする方法ではなく、コレクションに適用するための変換に焦点を当てた宣言型スタイルを優先して推奨されていません。

2.2. Java8以降

Java 8では、開発者がプログラミングのより宣言型のスタイルを持つために使用できるいくつかの構造が導入されました。 たとえば、ラムダ式の導入により、プログラマーはコレクションのすべての要素に適用する関数またはプロシージャを指定できます。

final long totalPassedAssertsInSucceededTests1 = results.stream()
    .filter(TestResult::isSucceeded)
    .mapToInt(TestResult::getSuccessfulAsserts)
    .sum();

ただし、反復の後続のステップを構成する機能がない場合、コードの可読性と保守性が向上しているにもかかわらず、状況は急速に悪化します。 コードがあいまいになり始めた状況で自分自身を見つけるのは簡単です:

results.stream().flatMap(
  res -> f(res).flatMap(
    res1 -> res1.flatMap(
      res2 -> res2.map( /* and so on */ ))));

それでは、Scalaがこのスタイルのプログラミングのソリューションにどのようにアプローチするかを見てみましょう。

3. Scalaでの宣言型ループ:For-Comprehension構造

for-comprehensionは、純粋に宣言型のスタイルを使用してコレクションを管理するScalaの方法です

val listOfPassedAssertsInSucceededTests: List[Int] =
  for {
    result <- results
    if result.succeeded
  } yield (result.successfulAsserts)
val passedAssertsInSucceededTests: Int = listOfPassedAssertsInSucceededTests.sum

一緒にすると、for-comprehension構文を形成するさまざまな構成を識別できます。 これは気が遠くなるように見えるかもしれませんが、このコードをウォークスルーして、各コンポーネントを段階的に見ていきましょう。

Scalaのfor-comprehensionの形式は、 for(列挙子)yieldeです。 列挙子は、キーワードのに続く括弧内に入る集合コードです。 列挙子は値を変数にバインドします for-comprehension body e は、列挙子によって生成されたすべての値を評価し、そのような値のシーケンスを作成します。

次のセクションでは、for-comprehensionの各コンポーネントについて詳しく見ていきます。

4. 列挙子

列挙子は、ジェネレーターまたはフィルターのいずれかです。 前の例では、両方のタイプの列挙子を含むfor-comprehensionがあります。 それぞれのタイプを詳しく見ていきましょう。

4.1. ジェネレーター

ステートメント結果<-結果ジェネレータを表します。 は、変数 resultsの各値をループする新しい変数、resultを導入します。 したがって、resultのタイプはTestResultです。

必要な数のジェネレーターを使用できます。 それらは互いに独立してループし、それらの変数のすべての可能な組み合わせを生成します。 この例では、結果のリストと実行時間のリストをループします。 次に、2つの要素をマージし、各テスト結果に対して実行されたアサートの総数と実行時間を一覧表示します。

val executionTimeList = List(("test 1", 100), ("test 2", 230))
val numberOfAssertsWithExecutionTime: List[(String, Int, Int)] =
  for {
    result <- results
    (id, time) <- executionTimeList
    if result.id == id
  } yield ((id, result.totalAsserts, time))

numberOfAssertsWithExecutionTimeリストに含まれる値は次のとおりです。

List[("test 1", 10, 100), ("test 2", 6, 230)]

for-comprehension内のすべてのジェネレーターは、ループする同じタイプを共有する必要があります。 前の例では、両方がリスト。 型変数はカウントされません。 したがって、タイプ List[TestResult]のジェネレーターとタイプList[(String、Int)]。のジェネレーターを混在させることができます。

4.2. フィルタ

for-comprehension 内では、フィルターの形式は ifboolean-conditionです。 フィルターは、ブール条件を尊重しないすべての値をブロックするガードとして機能します。

フィルタ内で、for-comprehensionのスコープで使用可能なすべての変数を使用してカスタムブール条件を作成できます。

前の例では、ジェネレーターによって宣言された変数を使用しました。 ただし、外部で宣言された変数を使用することもできます理解のために。 例を見てみましょう:

val hugeNumberOfAssertsForATest: Int = 10
val resultsWithAHugeAmountOfAsserts: List[TestResult] =
  for {
    result <- results
    if result.totalAsserts >= hugeNumberOfAssertsForATest
  } yield (result)

5. 理解のためのボディ

すでに述べたように、 for-comprehension bodyは、列挙子によって生成されたすべての値を評価し、そのような値のシーケンスを作成します。 本文内では、for-comprehensionの範囲内にある任意の変数または値を使用できます。

val magic: Int = 42
for {
  res <- result
} yield res * magic

イールドボディのタイプは、私たちが望むものなら何でもかまいません。これまで、私たちの例は、for-comprehensionの結果として何かを返します。 ただし、次のように評価される式を使用して、何も返さないようにすることは可能です。 単位。 たとえば、 収率ジェネレーターによってバインドされた変数を出力する本体:

for {
  res <- result
} println(s"The result is $res")

利回り本体が単位と評価される場合、利回りキーワードを省略できます。

6. 理解のために:ディープダイブ

前の例では、 for -comprehensionのセマンティクスがストリームまたはシーケンスに対する一連の操作のセマンティクスとどのように等しいかを確認しました。 Scalaでは、 for-comprehensionは、1つ以上のメソッドへの一連の呼び出しに対する構文糖衣にすぎません。

  • foreach
  • 地図
  • flatMap
  • withFilter

このようなメソッドを定義するすべてのタイプで、for-comprehension構文を使用できます。 例を見てみましょう。

まず、作業するクラスを定義しましょう。 式の結果のラッパーであるResultクラスを作成しましょう。

case class Result[A](result: A)

まず最初に、 for-comprehension:を使用して、Resultの値を標準出力に出力しようとします。

val result: Result[Int] = Result(42)
for {
  res <- result
} println(res)

コンパイラは、 for-comprehension:で変数resを使用できないことを警告します。

Value foreach is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].

そのため、Scalaコンパイラーは、上記の構成をforeachメソッドの呼び出しにデシュガーします。 Resultタイプに追加してみましょう。

def foreach(f: A => Unit): Unit = f(result)

さて、利回りの体にいくらかの尊厳を与えることを試みる時が来ました。 たとえば、いくつかの関数を適用して結果を変更します。

for {
  res <- result
} yield res * 2

今回、コンパイラは次のエラーを出します。

Value map is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].

Scalaコンパイラーは、mapメソッドを呼び出してyield本体を脱糖しようとしています。そのようなメソッドを定義しましょう。

def map[B](f: A => B): Result[B] = Result(f(result))

私たちは、新しいResultタイプがとても気に入っています。 for-comprehensionで多くのジェネレーターを使用して複数の結果を組み合わせたいと思います。 やってみましょう:

val anotherResult: Result = 100
for {
  res <- result
  another <- anotherResult
} yield res + another

これまでに定義したメソッドは、Scalaコンパイラーには不十分であり、新しいエラーが表示されます。

Value flatMap is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].

flatMap メソッドが必要な理由はまだわかりませんが、コンパイラーからの要求に応じてを追加します:

def flatMap[B](f: A => Result[B]): Result[B] = f(result)

しかし、なぜ for-comprehensionを使用するためにflatMap メソッドを定義する必要があるのでしょうか? 上記のfor-comprehensionの脱糖バージョンは次のとおりです。

result
  .flatMap(res =>
    anotherResult
      .map(another => res + another)
  )

Result タイプで実行する最後の操作は、for-comprehension内でフィルターを使用することです。 まず、空の結果を表す値を定義する必要があります。

object EmptyResult extends Result[Null](null)

フィルタを使用してみましょう:

for {
  res <- result
  if res == 10
} yield res

いつものように、コンパイラはメソッドが欠落していることを警告します。

Value withFilter is not a member of com.baeldung.scala.forcomprehension.ForComprehension.Result[Int].

ここでも、欠落しているメソッドを定義します。

def withFilter(f: A => Boolean): Result[_] = if (f(result)) this else EmptyResult

この場合、実装を少し強制します。 空の結果はResult[Null] のインスタンスであり、 Result [Nothing] のインスタンスではないため、 Result[A]のインスタンスは返されません。 ]。

7. 結論

この記事では、Scalaプログラミング言語で利用可能なfor-comprehensionコンストラクトを確認しました。

宣言型命令型のループスタイルの違いを示しました。 次に、 for-comprehension がどのように作成されるかを分析し、enumeratorsyield本体の両方の主な機能について説明しました。

最後に、 for-comprehension は、メソッド foreach、map、flatMap、、およびwithFilterへの一連の呼び出しの単なる構文糖衣であることを発見しました。

いつものように、すべてのコード実装はGitHub利用できます。