我试图理解这里列举的一个素数算法:https://wiki.haskell.org/index.php?title=Prime_numbers&oldid=36858#Postponed_Filters_Sieve,具体来说:
primes :: [Integer]
primes = 2: 3: sieve (tail primes) [5,7..]
where
sieve (p:ps) xs = h ++ sieve ps [x | x <- t, x `rem` p /= 0]
-- or: filter ((/=0).(`rem`p)) t
where (h,~(_:t)) = span (< p*p) xs
所以从概念上讲,我理解这个算法是如何工作的(筛选Erastothenes),从2,3开始,然后是一个数字列表,然后消除任何大于前一个方格的数据,并且可以被任何低于它的数字整除。
但是我很难跟上嵌套的递归步骤(素数上的主要calles筛子,它会在素数上筛选......)
据我所知,这是因为懒惰的评估而起作用,并且它可以产生正确的结果,但我无法跟随它。
例如,如果我要运行take 5 primes
实际发生的事情:
例如(为了便于阅读/推理,我会将拍摄操作的结果称为t):
第1步)
primes返回一个列表[2,3, xs]
所以t
是[2,3, take 3 xs]
其中xs
为sieve (tail primes) [5,7..]
第2步)
tail primes
为3:xs
其中xs
是sieve (tail primes) [5,7..]
等
所以现在应该是[2,3,3,3,3,3 ...]
筛子本身没什么问题......
所以我想我有两个问题。
1)这个算法究竟是如何工作的,以及我的跟踪错误在哪里/为何
2)通常,在Haskell中是否有办法弄清楚运行的顺序是什么?也许打印一个递归树?或者至少在调试器中停止?
答案 0 :(得分:3)
我冒昧地去优化并澄清了算法:
primes :: [Integer]
primes = 2 : sieve primes [3 ..]
sieve :: [Integer] -> [Integer] -> [Integer]
sieve [] xs = xs -- degenerate case for testing
sieve (p:ps) xs = h ++ sieve ps [x | x <- t, x `rem` p /= 0]
where (h, t) = span (< p*p) xs
这是相同的基本逻辑,但它会比您提供的版本执行更多冗余工作(每个输出值的常数因子)。我认为这是一个更简单的起点,一旦你理解了这个版本是如何工作的,很容易看出优化的作用。我还将sieve
拉到了自己的定义中。它没有使用其封闭范围内的任何东西,并且单独测试它的能力可能有助于理解正在发生的事情。
如果您想了解评估的进展情况,可以使用Debug.Trace
模块。我最常用的两个函数是trace
和traceShow
,具体取决于我想要查看的值。
所以,让我们从sieve
获取一些跟踪信息:
import Debug.Trace
primes :: [Integer]
primes = 2 : sieve primes [3 ..]
sieve :: [Integer] -> [Integer] -> [Integer]
sieve [] xs = trace "degenerate case for testing" xs
sieve (p:ps) xs = traceShow (p, h) $ h ++ sieve ps [x | x <- t, x `rem` p /= 0]
where (h, t) = span (< p*p) xs
并测试出来:
ghci> take 10 primes
[2(2,[3])
,3(3,[5,7])
,5,7(5,[11,13,17,19,23])
,11,13,17,19,23(7,[29,31,37,41,43,47])
,29]
嗯,这比预期的要清楚得多。当ghci打印出结果时,它会使用Show
实例作为结果的类型。 Show
的{{1}}实例本身就是懒惰的,因此列表的打印与跟踪交错。为了做得更好,让ghci产生一个不会输出的值,直到跟踪完成为止。 [Integer]
应该:
sum
那是......不太有用。追踪去哪儿了?好吧,请记住,跟踪功能非常不纯。他们明确的目标是产生副作用。但GHC并不尊重副作用。它假设所有功能都是纯粹的。该假设的一个结果是它可以存储评估表达式的结果。 (是否这样做取决于是否存在共享引用或CSE优化。在这种情况下,ghci> sum $ take 10 primes
129
本身是共享引用。)
也许如果我们要求它进一步评估到目前为止呢?
primes
好的,跟踪根据需要与ghci的输出分开。但在那一点上,它并没有真正提供丰富的信息。为了获得更好的图片,需要从头开始。要做到这一点,我们需要让ghci卸载质数的定义,以便从头开始重新评估它。有很多方法可以做到这一点,但我将展示一种方法,它有一些其他有用的方法。
ghci> sum $ take 20 primes
(11,[53,59,61,67,71,73,79,83,89,97,101,103,107,109,113])
639
通过在ghci> :load *sieve.hs
[1 of 1] Compiling Main ( sieve.hs, interpreted )
Ok, modules loaded: Main.
命令中将*
放在文件名前面,我指示ghci从头开始解释源,无论其当前状态如何。这适用于这种情况,因为它强制重新解释,即使源没有改变。当您想在当前目录中已编译输出的源上使用:load
并使其解释整个模块时,它也很有用,而不仅仅是加载已编译的代码。
:load
现在,让我们了解算法的实际工作方式。首先要看一下跟踪输出的组件是什么。第一个元素是素数,其倍数从潜在输出中筛选出来。第二个元素是被接受为素数的值列表,因为它们小于ghci> sum $ take 10 primes
(2,[3])
(3,[5,7])
(5,[11,13,17,19,23])
(7,[29,31,37,41,43,47])
129
,并且已经从候选列表中删除了小于该值的所有非素数。任何对Eratosthenes筛子的研究都应该熟悉其中的机制。
p*p
的来电始于sieve
。批判性地发挥作用的第一个懒惰是第一个参数的模式匹配。 sieve primes [3..]
构造函数已知,因此模式将(:)
与文字p
匹配,并将2
与未评估的表达式匹配。它的评估非常重要,因为ps
的调用是计算价值的。如果强制它被评估继续,它将引入循环数据依赖,这导致无限循环。
如跟踪所示,用于从候选项中删除元素的素数为2.对sieve
的调用将输入span
拆分为[3..]
。 ([3], [4..])
为h
,如跟踪输出所示。因此,调用[3]
的结果是sieve
。这是懒惰在算法中发挥作用的第二个位置。 [3] ++ <recursive call to sieve>
的实现与第二个参数完全没有任何关系,直到它已经产生了列表的前缀。这意味着在评估(++)
的递归调用之前,已知sieve
指的是评估为ps
的thunk。
有足够的信息来处理对[3] ++ <recursive call>
的递归调用。现在,sieve
与p
匹配,3
与thunk匹配,逻辑继续。跟踪应该说明此时的情况。
现在,您开始使用的版本需要做一些优化。首先,它观察到ps
的第一个元素总是等于t
,并且它使用模式匹配来消除该元素而不对其进行任何余数计算。这是每次检查的一个小额保存,但它是一个明确的节省。
其次,它会跳过过滤掉两个的倍数,而不是首先生成它们。这会减少生成的元素数量,以便稍后过滤两倍,和它会将每个奇数元素应用的滤波器数量减少一个。
另外,请注意堆叠过滤器行为实际上具有算法意义,并且不忠实于文献中描述的Eratosthenes筛子。有关此问题的进一步讨论,请参阅Melissa O&#39; Neill的The Genuine Sieve of Eratosthenes。