Scala:对于表达式显示意外行为



我们使用 Scalafor表达式,并迭代具有多个循环的元素,例如我们的First 表达式代码是:

for {
a <- 1 to 10
_ = print(a)
b <- 11 to 20
_ = print(b)
} yield 1

根据 scala,for表达式包含a <- 1 to 10等表达式,如果存在多个表达式,则将它们视为内部循环。我们假设上面的代码与我们的第二个表达式相同:

for(a <- 1 to 10) {
print(a + " --- ")
for(b <- 11 to 20) {
print(b + " === ")
}
} 

但是两个代码的输出是不同的。两个表达式代码的预期输出如下所示:

1 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 2 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 3 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 4 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 5 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 6 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 7 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 8 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 9 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 10 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 ===

但此输出仅由for表达式生成。第一个表达式给了我们意想不到的输出。

1 --- 2 --- 3 --- 4 --- 5 --- 6 --- 7 --- 8 --- 9 --- 10 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 ===

此输出首先打印1 2 .. 10元素,然后11 12 ... 20元素 10 次。为什么这两个输出不同?

第二件事,同时使用另一个for表达式:

for {
a <- 1 to 10
_ = print(a + " --- ")
b <- 11 to 20
_ = print(b + " === " + a + "  ")
} yield 1

输出为:

1 --- 2 --- 3 --- 4 --- 5 --- 6 --- 7 --- 8 --- 9 --- 10 --- 11 === 1  12 === 1  13 === 1  14 === 1  15 === 1  16 === 1  17 === 1  18 === 1  19 === 1  20 === 1  11 === 2  12 === 2  13 === 2  14 === 2  15 === 2  16 === 2  17 === 2  18 === 2  19 === 2  20 === 2  11 === 3  12 === 3  13 === 3  14 === 3  15 === 3  16 === 3  17 === 3  18 === 3  19 === 3  20 === 3  11 === 4  12 === 4  13 === 4  14 === 4  15 === 4  16 === 4  17 === 4  18 === 4  19 === 4  20 === 4  11 === 5  12 === 5  13 === 5  14 === 5  15 === 5  16 === 5  17 === 5  18 === 5  19 === 5  20 === 5  11 === 6  12 === 6  13 === 6  14 === 6  15 === 6  16 === 6  17 === 6  18 === 6  19 === 6  20 === 6  11 === 7  12 === 7  13 === 7  14 === 7  15 === 7  16 === 7  17 === 7  18 === 7  19 === 7  20 === 7  11 === 8  12 === 8  13 === 8  14 === 8  15 === 8  16 === 8  17 === 8  18 === 8  19 === 8  20 === 8  11 === 9  12 === 9  13 === 9  14 === 9  15 === 9  16 === 9  17 === 9  18 === 9  19 === 9  20 === 9  11 === 10  12 === 10  13 === 10  14 === 10  15 === 10  16 === 10  17 === 10  18 === 10  19 === 10  20 === 10

a的期望值存在于内部循环中。我们仍然对这种行为感到困惑。这种行为的共鸣是什么?

原因是:

for {
a <- 1 to 10
_ = print(a)
b <- 11 to 20
_ = print(b)
} yield 1

相当于:

(1 to 10).
map{a => print(a); a}.
flatMap{a => 
(11 to 20).
map{b => print(b); 1}
}

您会看到,由于1 to 10是一个集合,因此映射它将立即构建新集合并执行所有print语句。 如果希望仅按需执行print(a),则可以将1 to 10更改为(1 to 10).view

第二个循环等效于:

(1 to 10).foreach { a =>
print(a + " --- ")
(11 to 20).foreach { b =>
print(b + " === ")
}
}

这解释了为什么它会在所有值之前打印ab

您的第二个问题相当于:

(1 to 10).map{a => print(a + " --- "); a}.
flatMap{ a =>
(11 to 20).map{ b =>
print(b + " === " + a + "  ")
1
}
}

希望输出现在有意义。

你应该读这个:http://docs.scala-lang.org/tutorials/FAQ/yield.html,这个:http://www.scala-lang.org/files/archive/spec/2.11/06-expressions.html#for-comprehensions-and-for-loops。

简而言之,您的第一个表达式和第二个表达式实际上非常不同,因为一个使用yield,而另一个不使用。具有yield的称为"for-comprehension",另一个称为"for-loop"。

For-comprehention表示为mapflatMap的组合(加上一些与当前讨论无关的其他结构(,for-loop表示为foreach

也就是说,编译器将你的第一个表达式转换为:

scala.Predef
.intWrapper(1)
.to(10)
.map[(Int, Unit), scala.collection.immutable.IndexedSeq[(Int, Unit)]]((a: Int) => {
val x$1 = scala.Predef.print(a.+(" --- "));
scala.Tuple2.apply[Int, Unit](a, x$1)
})(scala.collection.immutable.IndexedSeq.canBuildFrom[(Int, Unit)])
.flatMap[Int, Any]((x$4: (Int, Unit)) =>
(x$4: @scala.unchecked) match {
case scala.Tuple2((a @ _), _) =>
scala.Predef
.intWrapper(11)
.to(20)
.map[(Int, Unit), scala.collection.immutable.IndexedSeq[(Int, Unit)]]((b: Int) => {
val x$2 = scala.Predef.print(b.+(" === "));
scala.Tuple2.apply[Int, Unit](b, x$2)
})(scala.collection.immutable.IndexedSeq.canBuildFrom[(Int, Unit)])
.map[Int, scala.collection.immutable.IndexedSeq[Int]]((x$3: (Int, Unit)) =>
(x$3: @scala.unchecked) match {
case scala.Tuple2((b @ _), _) => 1
})(scala.collection.immutable.IndexedSeq.canBuildFrom[Int])
})(scala.collection.immutable.IndexedSeq.canBuildFrom[Int])

可以简化为:

(1 to 10).map { a =>
val x = print(a + " --- ")
(a, x)
}.flatMap { case (_, _) =>
(11 to 20).map { b =>
val x = print(b + " === ")
(b, x)
}.map { _ =>
1
}
}

而第二个表达式为:

(1 to 10).foreach(a => {
print(a + " --- ")
(11 to 20).foreach(b => print(b + " === "))
})

在这里,表达式之间的差异很容易看出。无论如何,我都会通过解释,特别是对于第一个表达式,因为它是最复杂的。

在第一个表达式中,首先执行此map

(1 to 10).map { a =>
val x = print(a + " --- ")
(a, x)
}

它打印1 --- 2 --- 3 --- 4 --- 5 --- 6 --- 7 --- 8 --- 9 --- 10 ---,正如预期的那样。现在,该表达式的结果是具有以下元素的集合:Tuple2[Int, Unit],生成的集合包含其中的 10 个:(1, ()), (2, ()), ... (10, ())

flatMap在该中间集合上执行。因此,flatMap下的函数执行 10 次,并打印 10 次以下内容:11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 ===

最终结果与问题相同,即第一个表达式打印:

1 --- 2 --- 3 --- 4 --- 5 --- 6 --- 7 --- 8 --- 9 --- 10 --- 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 === 11 === 12 === 13 === 14 === 15 === 16 === 17 === 18 === 19 === 20 ===

我不会介绍第二个表达式,因为它要简单得多,并且表示 OP 预期的行为。

所以,

for { 
a <- 1 to 10
_ = print("something") 
b <- 11 to 20
_ = print("something else") 
} yield 1

相当于:

(1 to 10).flatMap { a =>  
print("something") 
(11 to 20).map { b => 
print("something else") 
1
}
}

现在,此循环的行为将取决于您用于第一个flatMap的集合类型。scala 中的一些集合是"懒惰的",其他的则不是。 "非懒惰"的集合是那些,当你这样做时foo.flatMap { x => ... }会立即计算整个集合的函数,并返回一个包含结果的新集合。惰性集合就是一个,它将在访问元素时逐个评估转换。

Seq(1,2,3,4)
.map { n => println("foo" + n); n + 1 }
.map { n => println("bar" + n); n - 1 }

这将打印:

foo1   
foo2
foo3
foo4
bar2
bar3
bar4
bar5

所有元素首先通过第一个地图,然后通过下一个地图。

另一方面,这:

Iterator(1,2,3,4)
.map { n => println("foo" + n); n + 1 }
.map { n => println("bar" + n); n - 1 }

根本不打印任何内容。 如果在生成的Iterator上调用.next,您将看到

foo1
bar2

因此,它通过两个映射发送第一个元素,然后再转到第二个映射。

同样的事情也会发生在你的循环中:(1 to 10)是一个Range,这是一个"渴望"的集合。这意味着,在进行2之前,for理解的整个主体都会被评估为1

您可以通过使集合"懒惰"来将行为切换到其他代码段:a <- (1 to 10).iteratora <- (1 to 10).toStream但是现在,for理解的结果也将是懒惰的 - 它将与列表中的第一个集合的类型相同,并且,为了看到整个打印出来的东西,你必须"强制"评估 al 元素,例如,通过调用结果.toList

最新更新