首先,尽管这是埃拉托塞尼筛子的一个实现,但这不是一个家庭作业问题。我可以在许多介绍书中找到Sieve的实现:)。
我的问题是,围绕我对引用透明性的困惑,这是函数式编程的优点之一。
我的理解是,我可以在程序中的任何地方用y
替换f(x)
,前提是y = f(x)
,并且我是在一个纯函数中。
使用筛子“手工”的第n个素数的简单实现是
nums0 = [2..] -- allowed by lazy evaluation
nums1 = [n | n <- nums0, mod n (head nums0) /= 0] -- works, gives [3,5,7,9,11,13,15...]
nums2 = [n | n <- nums1, mod n (head nums1) /= 0] -- works, gives [5,7,11,13,...]
nums3 = [n | n <- nums2, mod n (head nums2) /= 0] -- works, gives [7,11,13,...]
字符串
很明显,这里有一个递归模式。让我们调用ndnp m
函数,该函数返回不能被第一个m
素数整除的大于1的自然数列表,即
nums0 = ndnp 0 -- (i.e. all numbers starting at 2)
nums1 = ndnp 1 -- (not divisible by 2)
nums2 = ndnp 2 -- (not divisible by 2 or 3)
型
为了找到第N个素数,我们可以简单地做head $ ndnp (n-1)
。
构造numsX
数组的示例具有递归结构:
-- ndnp: Not Divisible by first N Primes
ndnp :: Integer -> [Integer]
ndnp 0 = [2..]
ndnp n = [n | n <- (ndnp (n-1)), mod n (head $ ndnp (n-1)) /= 0]
型
特别是,ndnp 1
是从ndnp 0
或nums0
构建的
当运行take 10 $ ndnp 0
时,我得到了我期望的答案。当我运行take 10 $ ndnp 1
时,我得到一个stackoverflow。
我的问题是:
- 我可以很好地分配:
a = ndnp 1
是法律的的评价是懒惰的,我理解。 - 当我尝试
take
时,我得到一个堆栈溢出(这表明它正在尝试计算整个结构)。我可以理解如果mod
或其他东西强迫渴望评估,但我知道这不是这样的,因为... take 10 nums1
工作得很好,它的构建步骤与循环遍历无限的、惰性求值的列表相同!- 既然是
nums0 = ndnp 0
,为什么take 10 $ nums1
的执行很好(它使用nums0
并迭代它),但take 10 $ ndnp 1
却坏了(它使用ndnp 0
的结果并迭代它)?引用透明性建议我应该能够将一个替换为另一个,但在一种情况下,我保留了惰性求值,而另一种情况下会生成堆栈溢出!
注意:我找到了一个解决方法
ndnp :: Integer -> [Integer]
ndnp 0 = [2..]
ndnp n = [n | n <- arr, mod n (head $ arr) /= 0]
where arr = ndnp (n-1)
型
这使我的代码工作,但没有给予我一个很好的概念性理解,当我有引用透明性,当我没有。
1条答案
按热度按时间67up9zun1#
如果你打开了警告(这是一个很好的实践-这对我来说有点神秘,很多编译器,包括GHC,默认为 * 不 * 打印警告),那么你会看到这样的东西:
字符串
这会让你知道哪里出了问题Haskell作用域规则意味着
ndnp
的原始定义等价于:型
也就是说,
n
的新定义 * 并没有 * 捕获第一次出现ndnp (n-1)
时的n
,但它捕获了解析中的所有其他n
。当你尝试计算
ndnp 1
时,你会得到:型
列表解析从
m = 2
开始,它在保护内调用ndnp (m-1) = ndnp 1
,导致无限循环。你可以通过重命名列表解析中新引入的迭代变量来解决这个问题:
型
观察到代入
n = 1
得到:型
然后代入
ndnp 0 = nums0
得到:型
它精确地匹配您对
nums1
的定义,直到变量替换。所以,在这个例子中没有违反引用透明性。
您找到的修复方法:
型
范围不同。Haskell作用域规则意味着它等价于:
型
看到新变量
m
如何捕获列表解析中m
的每次出现,但 * 不 * 捕获arr
定义中的n
了吗?这意味着这相当于:型
其中
ndnp (n-1)
都没有被捕获,这使得它等同于我上面的“固定”版本,直到变量重命名:型