我对Scala很新,所以请原谅我的无知!我正在尝试迭代以最大值为界的整数对。例如,如果最大值为5,则迭代应返回:
(0, 0), (0, 1), ..., (0, 5), (1, 0), ..., (5, 5)
我选择尝试以递归方式将其作为Stream返回:
@tailrec
def _pairs(i: Int, j: Int, maximum: Int): Stream[(Int, Int)] = {
if (i == maximum && j == maximum) Stream.empty
else if (j == maximum) (i, j) #:: _pairs(i + 1, 0, maximum)
else (i, j) #:: _pairs(i, j + 1, maximum)
}
没有tailrec注释,代码可以工作:
scala> _pairs(0, 0, 5).take(11)
res16: scala.collection.immutable.Stream[(Int, Int)] = Stream((0,0), ?)
scala> _pairs(0, 0, 5).take(11).toList
res17: List[(Int, Int)] = List((0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (1,0), (1,1), (1,2), (1,3), (1,4))
但这对我来说还不够好。编译器正确地指出_pairs的最后一行没有返回_pairs:
could not optimize @tailrec annotated method _pairs: it contains a recursive call not in tail position
else (i, j) #:: _pairs(i, j + 1, maximum)
^
所以,我有几个问题:
在Python(!)中,我想要的只是:
In [6]: def _pairs(maximum):
...: for i in xrange(maximum+1):
...: for j in xrange(maximum+1):
...: yield (i, j)
...:
In [7]: p = _pairs(5)
In [8]: [p.next() for i in xrange(11)]
Out[8]:
[(0, 0),
(0, 1),
(0, 2),
(0, 3),
(0, 4),
(0, 5),
(1, 0),
(1, 1),
(1, 2),
(1, 3),
(1, 4)]
感谢您的帮助!如果您认为我需要阅读参考文献/ API文档/其他任何内容,请告诉我,因为我很想学习。
答案 0 :(得分:26)
假设您正在制作列表而不是流: (让我用一个更简单的函数来说明我的观点)
def foo(n: Int): List[Int] =
if (n == 0)
0 :: Nil
else
n :: foo(n - 1)
在这种递归的一般情况下,foo(n - 1)
返回后,函数必须对它返回的列表做一些事情 - 它必须将另一个项连接到列表的开头。所以函数不能是尾递归的,因为在递归之后必须对列表进行一些操作。
如果没有尾递归,对于某些较大的n
值,就会耗尽堆栈空间。
通常的解决方案是将ListBuffer
作为第二个参数传递,然后填写。
def foo(n: Int) = {
def fooInternal(n: Int, list: ListBuffer[Int]) = {
if (n == 0)
list.toList
else {
list += n
fooInternal(n - 1, list)
}
}
fooInternal(n, new ListBuffer[Int]())
}
你正在做的事情被称为“tail recursion modulo cons”,这是一个由 LISP Prolog编译器自动执行的优化,当他们看到尾部递归模数模式时,因为它是如此共同。 Scala的编译器不会自动优化它。
Streams不需要尾递归以避免耗尽堆栈空间 - 这是因为它们使用一个聪明的技巧来阻止在代码中出现的foo
执行递归调用。函数调用包含在thunk中,并且仅在您实际尝试从流中获取值时调用。 1}}一次只能调用一次 - 它永远不会递归。
我在Stackoverflow上写了一篇解释how the #::
operator works的先前答案。以下是调用以下递归流函数时发生的情况。 (它在数学意义上是递归的,但它不会像你通常期望的那样在函数调用中进行函数调用。)
foo
您调用def foo(n: Int): Stream[Int] =
if (n == 0)
0 #:: Nil
else
n #:: foo(n - 1)
,它返回一个已经计算过一个元素的流,尾部是一个thunk,下次需要流中的元素时将调用foo(10)
。现在不调用foo(9)
- 而是将调用绑定到流中的foo(9)
,并且lazy val
立即返回。当您最终需要流中的第二个值时,将调用foo(10)
,并计算一个元素并将hte流的尾部设置为将调用foo(9)
的thunk。 foo(8)
会立即返回,而不会调用foo(9)
。等等...
这允许您创建无限流而不会耗尽内存,例如:
foo(8)
(请注意您在此流上调用的操作。如果您尝试执行def countUp(start: Int): Stream[Int] = start #::countUp(start + 1)
或forEach
,则会填满整个堆,但使用map
是使用流的任意前缀的好方法。)
为什么不使用Scala的take
循环,而不是处理递归和流?
for
这将在内存中实现整个集合,并返回def pairs(maximum:Int) =
for (i <- 0 to maximum;
j <- 0 to maximum)
yield (i, j)
。
如果您需要专门使用Stream,则可以将第一个范围转换为IndexedSeq[(Int, Int)]
。
Stream
这将返回def pairs(maximum:Int) =
for (i <- 0 to maximum toStream;
j <- 0 to maximum)
yield (i, j)
。当您访问序列中的某个点时,它将被实现到内存中,只要您仍然在该元素之前引用流中的任何点,它就会一直存在。
通过将两个范围转换为视图,您可以获得更好的内存使用率。
Stream[(Int, Int)]
返回def pairs(maximum:Int) =
for (i <- 0 to maximum view;
j <- 0 to maximum view)
yield (i, j)
,每次需要时计算每个元素,并且不存储预先计算的结果。
你也可以用同样的方式获得一个迭代器(你只能遍历一次)
SeqView[(Int, Int),Seq[_]]
返回def pairs(maximum:Int) =
for (i <- 0 to maximum iterator;
j <- 0 to maximum iterator)
yield (i, j)
。
答案 1 :(得分:2)
也许Iterator更适合你?
class PairIterator (max: Int) extends Iterator [(Int, Int)] {
var count = -1
def hasNext = count <= max * max
def next () = { count += 1; (count / max, count % max) }
}
val pi = new PairIterator (5)
pi.take (7).toList