【问题标题】:Learning Haskell: Seemingly Circular Program - Please help explain学习 Haskell:看似循环的程序 - 请帮助解释
【发布时间】:2011-03-31 04:42:13
【问题描述】:

我目前正在阅读 Doets 和 Van Eijck 所著的《通往逻辑、数学和编程的 Haskell 之路》一书。在这本书之前,我从未接触过任何函数式编程语言,所以请记住这一点。

在本书的早期,它给出了以下素数测试代码:

ldp :: Integer -> Integer
ldp n = ldpf primes1 n

ldpf :: [Integer] -> Integer -> Integer
ldpf (p:ps) n | rem n p == 0 = p 
              | p^2 > n      = n
              | otherwise    = ldpf ps n

primes1 :: [Integer]
primes1 = 2 : filter prime [3..]

prime :: Integer -> Bool
prime n | n < 1     = error "not a positive integer"
        | n == 1    = False 
        | otherwise = ldp n == n

ldp 调用 primes1,调用 prime,再调用 ldp,这似乎是一个循环编程。虽然这本书确实提供了这个作为程序执行和终止原因的解释:

ldp 调用 primes1,即素数列表。这是“惰性列表”的第一个示例。该列表被称为“惰性”列表,因为我们只计算列表中需要进一步处理的部分。要定义 primes1,我们需要对素数进行测试,但该测试本身是根据函数 LD 定义的,该函数又引用 primes1。我们似乎在绕圈子跑。通过避免对 2 的素数测试,可以使这个循环变得非恶性。如果给定 2 是素数,那么我们可以在 LD 中使用 2 的素数检查 3 是否为素数,依此类推,我们就成功了并运行

虽然我想我理解这个解释,但如果有人能用外行的话解释一下,我将不胜感激:

  1. 什么是“惰性列表”以及它在这种情况下如何应用?
  2. 知道 2 是素数如何使程序不具有恶意?

非常感谢任何答案。

【问题讨论】:

    标签: haskell primes lazy-evaluation circular-reference primality-test


    【解决方案1】:

    首先要注意的是,代码本身什么也不做。它只是一堆数学表达式,在我们尝试从中提取一些值之前,它的循环程度并不重要。我们如何做到这一点?

    我们可以这样做:

    print $ take 1 primes1
    

    这里没有问题。我们可以在不查看任何递归代码的情况下获取 primes1 的第一个值,它明确地作为 2 存在。那么:

    print $ take 3 primes1
    

    让我们尝试扩展primes1 以获取一些值。为了保留这些 表达式易于管理,我现在忽略了 printtake 部分,但是 请记住,我们只是因为他们才做这项工作。 primes1 是:

    primes1 = 2 : filter prime [3..]
    

    我们有我们的第一个值,我们的第一步是扩展过滤器。 如果这是一种严格的语言,我们将首先尝试扩展 [3..] 并得到 卡住。过滤器的一个可能定义是:

    filter f (x:xs) = if f x then x : filter f xs else filter f xs
    

    给出:

    primes1 = 2 : if prime 3 then 3 : filter prime [4..] else filter prime [4..]
    

    我们的下一步必须弄清楚prime 3 是真还是假,所以让我们 使用我们对primeldpldpf 的定义来扩展它。

    primes1 = 2 : if ldp 3 == 3 then 3 : filter prime [4..] else filter prime [4..]
    primes1 = 2 : if ldpf primes1 3 == 3 then 3 : filter prime [4..] else filter prime [4..]
    

    现在,事情变得有趣了,primes1 引用自己,而 ldpf 需要第一个值 做它的计算。幸运的是,我们可以轻松获得第一个值。

    primes1 = 2 : if ldpf (2 : tail primes) 3 == 3 then 3 : filter prime [4..] else filter prime [4..]
    

    现在,我们在 ldpf 中应用保护子句并找到 2^2 &gt; 3,因此找到 ldpf (2 : tail primes) 3 = 3

    primes1 = 2 : if 3 == 3 then 3 : filter prime [4..] else filter prime [4..]
    primes1 = 2 : 3 : filter prime [4..] 
    

    我们现在有了第二个值。请注意,我们评估的右侧从未变得特别大 并且我们不需要很远地遵循递归循环。

    primes1 = 2 : 3 : if prime 4 then 4 : filter prime [5..] else filter prime [5..]
    primes1 = 2 : 3 : if ldp 4 == 4 then 4 : filter prime [5..] else filter prime [5..]
    primes1 = 2 : 3 : if ldpf primes1 4 == 4 then 4 : filter prime [5..] else filter prime [5..]
    primes1 = 2 : 3 : if ldpf (2 : tail primes1) 4 == 4 then 4 : filter prime [5..] else filter prime [5..]
    

    我们使用保护子句rem 4 2 == 0,因此我们在这里得到2。

    primes1 = 2 : 3 : if 2 == 4 then 4 : filter prime [5..] else filter prime [5..]
    primes1 = 2 : 3 : filter prime [5..]
    primes1 = 2 : 3 : if prime 5 then 5 : filter prime [6..] else filter prime [6..]
    primes1 = 2 : 3 : if ldp 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
    primes1 = 2 : 3 : if ldpf primes1 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
    primes1 = 2 : 3 : if ldpf (2 : tail primes) 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
    

    没有守卫匹配,所以我们递归:

    primes1 = 2 : 3 : if ldpf (tail primes) 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
    primes1 = 2 : 3 : if ldpf (3 : tail (tail primes)) 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
    

    现在3^2 &gt; 5,所以表达式是 5。

    primes1 = 2 : 3 : if 5 == 5 then 5 : filter prime [6..] else filter prime [6..]
    primes1 = 2 : 3 : 5 : filter prime [6..]
    

    我们只需要三个值,所以评估可以停止。

    那么,现在回答您的问题。惰性列表是只需要我们评估所需内容的列表,并且 2 有帮助,因为它确保当我们到达递归步骤时,我们总是已经有足够的值 计算出来的。 (想象一下,如果我们不知道 2,扩展这个表达式,我们最终会陷入一个循环 很快。)

    【讨论】:

      【解决方案2】:

      Haskell 的一个决定性特征是它的惰性。列表只是这个故事的一部分。基本上,Haskell 永远不会执行 任何 计算,直到:

      1. 计算的值是需要来计算其他需要到...
      2. 你告诉 Haskell 比其他方式更快地计算一些东西。

      所以primes1 函数会生成Integer 值的列表,但它不会生成满足整体计算所需的任何值。即便如此,如果你这样定义:

      primes1 :: [Integer]
      primes1 = filter prime [2..]
      

      你会遇到问题。 primes1 将尝试通过(间接)评估prime 2 来生成其序列中的第一个值,这将评估ldp 2,这将要求primes1 生成的第一个值...presto 无限循环!

      通过直接返回 2 作为primes1 生成的序列的第一个值,可以避免无限循环。对于序列中生成的每个后续值,primes1 已经生成了一个先前的值,ldp 将评估该值作为计算后续值的一部分。

      【讨论】:

        【解决方案3】:

        按顺序:

        懒惰是只在需要时才评估表达式的属性,而不是在可能的时候。惰性列表可能是无限的。如果评估不是惰性的,那么尝试评估无限列表显然是个坏主意。

        我不确定“非恶意”是什么意思,但我想你会发现将值“2”作为已知素数为递归提供了一个基本情况,即它提供了程序将终止的特定输入。编写一个递归函数(或者实际上是一组相互递归的函数)通常涉及在应用的连续步骤中针对这个基本情况减少一些输入值。

        作为参考,这种形状的程序片段通常称为相互递归。在这种情况下,您不会真正应用“循环引用”一词。

        【讨论】:

        • 一个恶性循环是一种不愉快的传递,例如。男孩 1 欺负男孩 2,男孩 2 告诉他的父亲,他很沮丧,对他的员工很刻薄,其中一个是男孩 1 的父亲,然后他把挫败感发泄到儿子身上,然后男孩欺负男孩 2... 非恶性循环是一个循环这不会带来不愉快。
        • 对,我对其他情况下的术语很熟悉,但我从未见过它应用于程序分析。这是我不熟悉的haskellism吗?
        • 这不是 Haskell 主义。 “恶性循环”是一个英语成语(是否是英语独有的,我不知道)。任何你希望成为循环的事件循环序列(无论是在生活中还是在编程中)都可以称为“恶性循环”。
        • 对。我没有注意到引用的文本也使用了该术语。我认为这可能是 OP 梦寐以求来描述这种情况的东西。无论如何,我认为这不是一个非常有用的术语。 “非终止”、“发散”或“无限循环”都是我很容易与这个概念相关联的术语,但是 :)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-21
        • 1970-01-01
        • 2018-06-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多