所以,我正在努力自学Scala,我一直在玩的其中一件事是Stream
课。我试图使用classic Haskell version of Dijkstra's solution对汉明数字问题的天真翻译:
object LazyHammingBad {
private def merge(a: Stream[BigInt], b: Stream[BigInt]): Stream[BigInt] =
(a, b) match {
case (x #:: xs, y #:: ys) =>
if (x < y) x #:: merge(xs, b)
else if (y < x) y #:: merge(a, ys)
else x #:: merge(xs, ys)
}
val numbers: Stream[BigInt] =
1 #:: merge(numbers map { _ * 2 },
merge(numbers map { _ * 3 }, numbers map { _ * 5 }))
}
在翻译中解决这个问题很快就让人失望:
scala> LazyHammingBad.numbers.take(10).toList
java.lang.StackOverflowError
我决定查看其他人是否使用Haskell方法解决了Scala中的问题,并从Rosetta Code中调整了this solution:
object LazyHammingGood {
private def merge(a: Stream[BigInt], b: Stream[BigInt]): Stream[BigInt] =
if (a.head < b.head) a.head #:: merge(a.tail, b)
else if (b.head < a.head) b.head #:: merge(a, b.tail)
else a.head #:: merge(a.tail, b.tail)
val numbers: Stream[BigInt] =
1 #:: merge(numbers map {_ * 2},
merge(numbers map {_ * 3}, numbers map {_ * 5}))
}
这个很好用,但我仍然想知道我在LazyHammingBad
中是怎么出错的。使用#::
来构建x #:: xs
会因某种原因强制评估xs
吗?有没有办法安全地使用模式匹配无限流,或者如果你不想让事情爆炸,你只需要使用head
和tail
吗?
答案 0 :(得分:19)
a match {case x#::xs =>...
与val (x, xs) = (a.head, a.tail)
大致相同。因此,坏版本和好版本之间的区别在于,在错误版本中,您在开始时正在调用a.tail
和b.tail
,而不是仅使用它们来构建尾部得到的流。此外,当你在#::
的右边使用它们时(不是模式匹配,但是在#:: merge(a.b.tail)
中构建结果,你实际上并没有调用merge),只有在访问尾部时才会进行返回的Stream。所以在好的版本中,对merge的调用根本不会调用tail
。在错误版本中,它会在开始时调用它。
现在,如果您考虑数字,甚至是简化版本,请说1 #:: merge(numbers, anotherStream)
,当您致电时,请致电tail
(take(10)
会),merge
待评估。您在tail
上致电numbers
,merge
呼叫numbers
作为参数,呼叫tails
numbers
,呼叫merge
},调用tail
...
相比之下,在超级懒惰的Haskell中,当你模式匹配时,它几乎没有任何工作。当您执行case l of x:xs
时,它会评估l
,足以知道它是空列表还是缺点。
如果它确实是一个缺点,那么x
和xs
将作为两个thunks提供,这些函数最终将提供对内容的访问。 Scala中最接近的等价物就是测试empty
。
另请注意,在Scala Stream中,虽然tail
是惰性的,但head
却不是。当你有一个(非空)流时,必须知道头部。这意味着当你得到流的尾部时,它本身就是一个流,它的头部,即原始流的第二个元素,必须被计算出来。这有时会有问题,但在你的例子中,你甚至在到达那里之前就失败了。
答案 1 :(得分:6)
请注意,您可以通过为Stream
:
我刚刚在Scala Worksheet:
中拉了一下object HammingTest {
// A convenience object for stream pattern matching
object #:: {
class TailWrapper[+A](s: Stream[A]) {
def unwrap = s.tail
}
object TailWrapper {
implicit def unwrap[A](wrapped: TailWrapper[A]) = wrapped.unwrap
}
def unapply[A](s: Stream[A]): Option[(A, TailWrapper[A])] = {
if (s.isEmpty) None
else {
Some(s.head, new TailWrapper(s))
}
}
}
def merge(a: Stream[BigInt], b: Stream[BigInt]): Stream[BigInt] =
(a, b) match {
case (x #:: xs, y #:: ys) =>
if (x < y) x #:: merge(xs, b)
else if (y < x) y #:: merge(a, ys)
else x #:: merge(xs, ys)
} //> merge: (a: Stream[BigInt], b: Stream[BigInt])Stream[BigInt]
lazy val numbers: Stream[BigInt] =
1 #:: merge(numbers map { _ * 2 }, merge(numbers map { _ * 3 }, numbers map { _ * 5 }))
//> numbers : Stream[BigInt] = <lazy>
numbers.take(10).toList //> res0: List[BigInt] = List(1, 2, 3, 4, 5, 6, 8, 9, 10, 12)
}
现在,您只需要确保Scala在进行模式匹配时找到object #::
而不是Stream.class
中的#>:
。为方便起见,最好使用其他名称,例如##::
或case Stream.Empty
,然后记住在模式匹配时始终使用该名称。
如果您需要匹配空流,请使用case Stream()
。使用{{1}}会尝试在模式匹配中评估整个流,这会导致悲伤。