1. 概述

在命令式编程语言中,我们通常使用 for 循环while 循环 来遍历集合。而 Scala 引入了一种新的循环结构:for-comprehension(for 推导式)

和其他许多 Scala 特性一样,for-comprehension 直接借鉴自 Haskell。它的用途远不止于遍历集合,它还能帮助我们以更函数式的方式简化语法复杂度。

在本篇文章中,我们将深入探讨 Scala 的 for-comprehension 结构。

2. Java 中的传统与声明式循环对比

为了更好地理解 Scala 的 for-comprehension,我们先快速回顾一下 Java 中的循环方式。

2.1. Java 8 之前

在 Scala 中,我们不太喜欢使用传统的循环结构。实际上,在 Scala 中几乎找不到带有副作用的 while 或 for 循环。

但在 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. Java 8 及之后

Java 8 引入了一些新的结构,让开发者可以写出更具声明式的代码。例如,lambda 表达式的引入允许我们将函数或过程应用到集合的每个元素上:

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 (enumerators) yield e。其中,enumerators 是紧跟 for 关键字之后括号内的代码块,它们负责将值绑定到变量。而 e 是 for-comprehension 的主体,会对每个由 enumerators 生成的值进行计算,最终形成一个值的序列。

接下来我们将逐一分析这些组成部分。

4. 枚举器(Enumerators)

枚举器可以是 generator(生成器)filter(过滤器)。在之前的例子中,我们同时使用了这两种枚举器。下面我们分别来看。

4.1. 生成器(Generators)

语句 result <- results 是一个生成器。它 **引入了一个新变量 result**,该变量会依次绑定到 results 集合中的每一个元素。因此,result 的类型是 TestResult

✅ 我们可以有 任意数量的生成器,它们 彼此独立地循环,产生所有变量的组合。例如:

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 类型。类型参数可以不同,比如 List[TestResult]List[(String, Int)] 是可以混用的。

4.2. 过滤器(Filters)

在 for-comprehension 内部,过滤器的形式是 if boolean-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 的主体会对每个由 enumerators 生成的值进行计算,并形成一个值的序列。在主体中,我们可以使用所有在作用域内的变量或值:

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

yield 主体的返回类型可以是任意类型。到目前为止,我们的例子都返回了某种值。但也可以返回 Unit,比如用于打印:

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

如果 yield 主体的返回类型是 Unit,那么可以省略 yield 关键字。

6. For-Comprehension 深入解析

前面的例子展示了 for-comprehension 在语义上等价于对集合进行一系列操作。✅ 在 Scala 中,for-comprehension 实际上是以下方法调用的语法糖

  • foreach
  • map
  • flatMap
  • withFilter

我们可以在任何定义了这些方法的类型上使用 for-comprehension。下面来看一个例子。

首先,我们定义一个简单的包装类:

case class Result[A](result: A)

尝试使用 for-comprehension 打印 Result 的值:

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

编译器会报错:

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

✅ 这是因为编译器将 for-comprehension 翻译成了对 foreach 方法的调用。我们添加该方法:

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

接着,我们尝试使用 yield

for {
  res <- result
} yield res * 2

编译器报错:

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

✅ 编译器将其翻译为对 map 方法的调用,我们继续添加:

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

如果我们想在 for-comprehension 中使用多个生成器:

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

编译器提示缺少 flatMap 方法:

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)

其等价于:

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

最后,我们尝试在 for-comprehension 中添加过滤器:

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

编译器提示缺少 withFilter 方法:

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

⚠️ 注意:这里我们返回的是 EmptyResult,它是 Result[Null] 的实例。

7. 总结

本文回顾了 Scala 中的 for-comprehension 结构。

我们比较了命令式与声明式循环风格的区别,然后分析了 for-comprehension 的结构,包括 枚举器yield 主体 的使用。

✅ 最后我们发现,for-comprehension 实际上是 foreachmapflatMapwithFilter 等方法调用的语法糖

一如既往,所有代码示例都可以在 GitHub 上找到。


原始标题:A Comprehensive Guide to For-Comprehension in Scala