所以我花了好几个小时试图弄清楚这段代码是如何生成素数的。
lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i =>
ps.takeWhile{j => j * j <= i}.forall{ k => i % k > 0});
我使用了许多印刷品等,但没有让它更清晰。
这就是我认为代码的作用:
/**
* [2,3]
*
* takeWhile 2*2 <= 3
* takeWhile 2*2 <= 4 found match
* (4 % [2,3] > 1) return false.
* takeWhile 2*2 <= 5 found match
* (5 % [2,3] > 1) return true
* Add 5 to the list
* takeWhile 2*2 <= 6 found match
* (6 % [2,3,5] > 1) return false
* takeWhile 2*2 <= 7
* (7 % [2,3,5] > 1) return true
* Add 7 to the list
*/
但如果我将列表中的j*j
更改为2 * 2,我假设将完全相同,则会导致堆栈溢出错误。
我显然在这里缺少一些基本的东西,并且真的可以使用某人向我解释这个,就像我五岁。
非常感谢任何帮助。
答案 0 :(得分:24)
我不确定寻求程序/命令性解释是获得理解的最佳方式。流来自函数式编程,从这个角度来看它们是最好的理解。您给出的定义的关键方面是:
lazy 。除了流中的第一个元素之外,在您要求之前不会计算任何内容。如果你从不要求第五个素数,它将永远无法计算。
这是递归。素数列表是根据其自身定义的。
无限。 Streams具有有趣的属性(因为它们很懒惰),它们可以表示具有无限数量元素的序列。 Stream.from(3)
就是一个例子:它代表了列表[3,4,5,...]。
让我们看看我们是否能理解你的定义为何计算素数序列。
该定义以2 #:: ...
开头。这只是说序列中的第一个数字是2 - 到目前为止足够简单。
下一部分定义其余的素数。我们可以从3(Stream.from(3)
)开始的所有计数数字开始,但我们显然需要过滤掉一堆这些数字(即所有复合数据)。那么让我们考虑每个数字i
。如果i
不是较低素数的倍数,则i
为素数。也就是说,对于所有素数i
小于k
,i
,i % k > 0
为素数。在Scala中,我们可以将其表达为
nums.filter(i => ps.takeWhile(k => k < i).forall(k => i % k > 0))
然而,实际上并不需要检查所有较小的素数 - 我们实际上只需要检查其平方小于或等于i
的素数(这是数论的一个事实{ {1}})。所以我们可以改写
*
所以我们推导出你的定义。
现在,如果您碰巧尝试了第一个定义(使用nums.filter(i => ps.takeWhile(k => k * k <= i).forall(k => i % k > 0))
),您会发现它不起作用。为什么不?它与这是一个递归定义的事实有关。
假设我们正在尝试决定序列中2之后的内容。该定义告诉我们首先确定3是否属于。为此,我们认为第一个素数列表大于或等于3(k < i
)。第一个素数是2,小于3 - 到目前为止一直很好。但是我们还不知道第二个素数,所以我们需要计算它。很好,所以我们首先要看看3是否属于...... BOOM!
takeWhile(k => k < i)
很容易看出,如果数字*
是复合数,那么其中一个因子的平方必须小于或等于n
。如果n
是复合的,那么按照定义n
,其中n == a * b
(我们可以通过恰当地标记这两个因素来保证1 < a <= b < n
)。从a <= b
开始,它跟随a <= b
,因此它跟随a^2 <= a * b
。
答案 1 :(得分:3)
你的解释大多是正确的,你只犯了两个错误:
takeWhile
不包含最后一个选中的元素:
scala> List(1,2,3).takeWhile(_<2)
res1: List[Int] = List(1)
您认为ps
始终只包含两个和三个,但由于Stream
是惰性的,因此可以向其中添加新元素。实际上,每次找到新的素数时,它都会添加到ps
,在下一步takeWhile
中会考虑这个新添加的元素。在这里,重要的是要记住Stream
的尾部仅在需要时计算,因此takeWhile
在forall
被评估为真之前无法看到它。
记住这两件事,你应该想出这个:
ps = [2]
i = 3
takeWhile
2*2 <= 3 -> false
forall on []
-> true
ps = [2,3]
i = 4
takeWhile
2*2 <= 4 -> true
3*3 <= 4 -> false
forall on [2]
4%2 > 0 -> false
ps = [2,3]
i = 5
takeWhile
2*2 <= 5 -> true
3*3 <= 5 -> false
forall on [2]
5%2 > 0 -> true
ps = [2,3,5]
i = 6
...
虽然这些步骤描述了代码的行为,但它并不完全正确,因为不仅向Stream
添加元素是惰性的,而且每个操作都在其上。这意味着当您调用xs.takeWhile(f)
时,并非所有值都会立即计算f
为false时的值 - 它们是在forall
想要查看它们时计算的(因为它是唯一的函数)在这里需要查看所有元素才能确定结果为true,因为false它可以先中止)。这里考虑到懒惰的计算顺序(例子只看9):
ps = [2,3,5,7]
i = 9
takeWhile on 2
2*2 <= 9 -> true
forall on 2
9%2 > 0 -> true
takeWhile on 3
3*3 <= 9 -> true
forall on 3
9%3 > 0 -> false
ps = [2,3,5,7]
i = 10
...
由于forall
在评估为false时被中止,takeWhile
不会计算剩余的可能元素。
答案 2 :(得分:1)
该代码更容易(对我来说,至少)阅读时将某些变量重命名为暗示,如
lazy val ps: Stream[Int] = 2 #:: Stream.from(3).filter(i =>
ps.takeWhile{p => p * p <= i}.forall{ p => i % p > 0});
这很自然地读取从左到右
primes 2 , 3 中的那些数字
i
, all primesp
,其正方形不超过i
,不均匀划分i
(即没有一些非零余数) )。
以一种真正的递归方式,要理解这个定义是为了定义不断增加的素数流,我们假设 这样,从这个假设我们看到没有出现矛盾,即定义的真实性成立。
此后唯一可能的问题是,在定义时访问流ps
的时间。作为第一步,假设我们只是从某个地方为我们提供了另一个素数流,神奇地。然后,在看到定义的真实性之后,检查访问的时间是否正常,即在定义之前我们从不尝试访问ps
的区域;这会使定义停滞不前,非生产性。
我记得在某个地方(不记得在哪里)阅读以下内容 - 学生和巫师之间的对话,
这是代码的伪代码 1 ,部分读取从右到左,为了清晰起见,再次重命名了一些变量(使用p
代表“素“):
ps = 2 : filter (\i-> all (\p->rem i p > 0) (takeWhile (\p->p^2 <= i) ps)) [3..]
也是
ps = 2 : [i | i <- [3..], and [rem i p > 0 | p <- takeWhile (\p->p^2 <= i) ps]]
使用list comprehensions更明显一点。 and
检查布尔列表中的所有条目是True
(读取|
为“for”,<-
为“from”,,
为“这样“(\p-> ...)
为”p
“的 lambda 。
所以你看,ps
是2的懒惰列表 ,然后是数字i
从流[3,4,5,...]
开始,对于从p
中抽取ps
的所有p^2 <= i
,i % p > 0
就是ps
。这实际上是最佳 trial division算法。 :)
当然,这里有一个微妙之处:列表p
是开放式的。我们使用它,因为它正在“充实”(当然,因为它是懒惰的)。当ps
取自ps
时,可能会出现这种情况,即我们已经超过了它的结尾,在这种情况下,我们手上会有一个非终止计算(“黑洞“)。它恰好发生了:)(并且需要/可以通过数学方式证明)这对于上述定义是不可能的。所以2无条件地放入bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- takeWhile (\p->p < i) bad]]
,所以开始时会有一些东西。
但如果我们试图“简化”,
takeWhile (\p->p < 3) bad
它只产生一个数字后停止工作,2:当考虑3作为候选人时,bad
要求{2}之后的bad = 2 : [i | i <- [3..], and [rem i p > 0 | p <- [2..(i-1)] ]]
中的下一个数字,但那里还没有更多的数字。它“超越自己”。
这是“固定”的
{{1}}
但这是一个慢得多的试验分割算法,距离最佳非常远。
-
1 (Haskell实际上,这对我来说更容易:))