【问题标题】:Does this Haskell example effectively demonstrate laziness?这个 Haskell 示例是否有效地证明了懒惰?
【发布时间】:2012-11-28 07:48:46
【问题描述】:

我是 Haskell 的新手,我正在为我的编程语言课写一篇关于它的论文。我想用一些示例代码来展示 Haskell 的懒惰,但我不确定我所看到的是否真的是懒惰。

doubleMe xs = [x*2 | x <- xs]

在 ghci 中:

let xs = [1..10]
import Debug.Trace
trace (show lst) doubleMe (trace (show lst) doubleMe (trace (show lst) doubleMe(lst)))

输出:

[1,2,3,4,5,6,7,8,9,10]
[1,2,3,4,5,6,7,8,9,10]
[1,2,3,4,5,6,7,8,9,10]
[8,16,24,32,40,48,56,64,72,80]

感谢您的宝贵时间和帮助!

【问题讨论】:

  • 如果你想显示惰性,为什么不直接使用无限列表,然后显示计算(首先获取n 值,搜索传递谓词的值等)?两个经典的例子是素数和斐波那契数列。
  • 感谢您的建议。如果我没有得到关于我现在拥有的代码的答案,我会调查这些。
  • 你认为你的代码示例为什么表现出惰性?我认为您可能将惰性与不变性混淆了。 (我认为lst 应该是xs。)
  • 您可能还对Why is lazy evaluation useful? 感兴趣。一个答案给出了nice examplehead . quickSort 仅具有 O(n) 复杂性,因为未计算排序列表的其余部分。

标签: haskell lazy-evaluation


【解决方案1】:

您在这里对trace 的使用并不是特别有见地,或者实际上根本没有。您所做的只是在评估的四个不同点打印出相同的列表,这并不能告诉您有关程序实际状态的任何信息。这里实际发生的是 trace 在计算甚至开始之前在每个加倍步骤中都被强制执行(当结果列表被请求为弱头范式时)。这与使用完全严格评估的语言几乎相同。

要看到一些懒惰,你可以这样做

Prelude Debug.Trace> let doubleLsTracing xs = [trace("{Now doubling "++show x++"}")$ x*2 | x<-xs]
Prelude Debug.Trace> take 5 $ doubleLsTracing [1 .. 10]
{Now doubling 1}
{Now doubling 2}
{Now doubling 3}
{Now doubling 4}
{Now doubling 5}
[2,4,6,8,10]

您可以看到只有五个数字加倍,因为只请求了五个结果;即使doubleLsTracing 给出的列表有 10 个条目。

请注意,trace 通常不是监视“执行流程”的好工具,它只是一种允许“查看”局部变量以查看某些函数中发生了什么的技巧。

【讨论】:

    【解决方案2】:

    简短的回答是“不”。 leftaroundabout 在他的回答中很好地解释了这一点。

    我的建议是:

    1. 阅读并理解lazy evaluation的定义。
    2. 编写一个函数,其中一个参数可以发散,这是一个无法在您最喜欢的严格(非惰性)语言(C、python、Java)中工作的示例。例如,sumIfFirstArgIsNonZero(x, y),如果 x != 0 则返回 x+y,否则返回 0。
    3. 对于加分,定义你自己的函数 ifThenElse 不使用 Haskell 的内置 if-then-else 语法,并解释为什么用惰性语言编写新的控制流结构很容易。

    这应该比试图围绕无限数据流或打结技巧更容易。

    【讨论】:

    • 大多数严格的语言都有非严格的 if-then-else,所以我不确定第二点是什么。
    • 也许返回x*y 会更有意义,因为那时x = 0 的行为是一致的?
    • 一个不太人为的例子可能是:使用letwhere 定义在某些输入上会发散的值,但仅在检查输入后使用这些值。这既避免了错误又避免了不必要的计算,无需将事物嵌套在巨大的条件块塔中。
    【解决方案3】:

    无限流总是一个很好的例子。如果没有特殊结构,您无法在其他语言中获得它们 - 但在 Haskell 中它们是非常自然的。

    一个例子是斐波那契流:

    fib = 0 : 1 : zipWith (+) fib (tail fib)
    
    take 10 fib => [0,1,1,2,3,5,8,13,21,34]
    

    另一种是通过试除法获得素数流:

    primes = sieve [2..]
        where sieve (x:xs) = x : filter (not . (== 0) . (`mod` x)) (sieve xs)
    
    take 10 primes => [2,3,5,7,11,13,17,19,23,29]
    

    此外,在 Haskell 中实现回溯非常简单,让您能够按需懒惰地获取解决方案列表:

    http://rosettacode.org/wiki/N-queens_problem#Haskell

    这里有一个更复杂的例子,展示了如何实现 min:

    Lazy Evaluation and Time Complexity

    它基本上展示了如何使用 Haskell 的惰性来获得 minimum 函数的非常优雅的定义(在元素列表中找到最小值):

    minimum = head . sort
    

    你可以通过人为的例子来证明 Haskell 的懒惰。但我认为展示惰性如何帮助您为常见问题开发解决方案要好得多,这些解决方案表现出比其他语言更大的模块化。

    【讨论】:

      【解决方案4】:

      懒惰的主要观点是不需要的值不会被计算——所以为了证明这一点,你必须展示没有被评估的东西。您的示例并不是展示惰性的最佳方式,因为最终会计算所有值。

      这是一个小例子,作为起点:

      someValueThatNeverTerminates = undefined -- for example, a divide-by-zero error
      
      main = do (greeting, _) = ("Hello terminating world!", someValueThatNeverTerminates)
                putStrLn greeting
      

      这立即打招呼——如果不是懒惰,整个事情都会中断。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-09-06
        • 2017-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-11-09
        • 2015-09-06
        相关资源
        最近更新 更多