在Haskell中列出惰性求值

在Haskell中列出惰性求值,haskell,lazy-evaluation,Haskell,Lazy Evaluation,为了理解Haskell中的列表惰性评估,我做了以下简单假设 head [1, 2] -- expr1 head [1 .. 2] -- expr2 head [1 ..] -- expr3 head . (1 :) $ [] -- eval1 head . (1 :) . (2 :) $ [] -- eval2 我想expr3会像eval1那样被惰性地评估,那么expr1和expr2呢

为了理解Haskell中的列表惰性评估,我做了以下简单假设

head [1, 2]                -- expr1
head [1 .. 2]              -- expr2
head [1 ..]                -- expr3

head . (1 :) $ []          -- eval1
head . (1 :) . (2 :) $ []  -- eval2
我想
expr3
会像
eval1
那样被惰性地评估,那么
expr1
expr2

一般来说,

  • Haskell中的延迟计算是编译时和运行时的一种技术吗
  • 在时间、空间复杂度或程序逻辑上,哪里说效率高但难以推理

    • 列表没有什么特别之处。它们只是递归数据类型:

      data [a] = a : [a] | []
      
      现在,当您使用
      [1..2]
      时,即直接变成一个列表
      (1:[])
      !,它存储为表达式
      [1..2]

      现在
      head
      定义为:

      head :: [a] -> a
      head (x:_) = x
      
      如果将
      head[1..2]
      调用到
      main
      (因此Haskell被迫对其求值),它将看到
      [1..2]
      不是一个数据结构,而是一个未解析的表达式,它将稍微解析表达式:

      [1 .. 2] to (1:[(succ 1) .. 2])
      
      因此,现在我们读到:

      head (1:[(succ 1) .. 2])
      
      (注意尾部仍然是一个表达式),但由于
      head
      只对“head”感兴趣,因此它将返回
      1
      。注意,如果
      是例如
      1+2
      ,它也不会立即将其计算为
      3

      此外,如果您只需调用
      head[1..2]
      表达式将不会自动计算,例如,仅当您希望显示结果时,Haskell才会进行计算

      根据编译器实现的不同,编译器可以在编译时传播常量(文字)并对其执行操作,但由于编译器应始终遵循执行标准,因此语义保持不变。

      术语“惰性评估”有多种用法

    • 惰性评估是一种语义;它说明了哪些表达式对哪些值求值。(不过,我承认语义中缺少一些细节!)
    • 惰性评价是一种实施策略;它提供了一种方法来“运行”符合上述语义的lambda演算术语,并使用共享来改进更明显的“按名称调用”实现策略的时间和内存使用
    • 我认为,你正在以第三种方式使用它
    • 在剩下的部分中,我将使用“惰性评估”作为实现策略,使用“非严格语义”作为语义

      我想
      expr3
      会像
      eval1
      那样被惰性地评估,那么
      expr1
      expr2

      非严格的语义规定,对所有五个术语的求值都应终止并产生值
      1
      ,因此任何符合性的实现都将以这种方式进行。对于每个表达式,延迟计算将在大约相同的空间和时间内完成此操作。如果您强制执行这五个术语中的任何一个,我希望GHC会选择延迟求值,尽管经过优化后,GHC可能会在编译时执行求值。如果您非常感兴趣,可以通过传递
      -ddump siml
      标志来检查这一点

      Haskell中的惰性计算在编译时和运行时都是一种技术吗

      希望上面的讨论已经澄清了这个问题。非严格语义描述编译时和运行时之间的特定连接(即,编译器必须生成一个程序,其运行时行为生成语义指定的值)。惰性评估是一种特殊的实现策略,用于生成符合语义的程序。GHC有时在其项目中使用惰性评估,但有时使用其他实施策略;但是,它符合非严格语义。(如果你找到一个没有的地方,那就是一个bug!)

      在时间、空间复杂度或程序逻辑上,哪里说效率高但难以推理


      非严格语义通常不会说明计算过程中使用了多少时间或空间,因此如果您想对此进行推理,则需要完全不同的技术。即使您决定将您的推理限制为使用惰性评估实现的程序,事情也可能会很困难。考虑像
      [1..]
      这样的表达式:这个表达式使用了多少空间?这个问题不能在真空中回答;惰性评估的基本思想是让值的消费者控制构建了多少值。因此,如果不了解程序如何处理表达式
      [1..]
      ,我们就无法了解太多。它可能会丢弃该值,在这种情况下几乎不使用任何空间;或者它可以沿着列表向下走,在这种情况下,会使用恒定的空间量;或者它可能在不同的时间遍历列表两次,在这种情况下使用无界空间;或者,它可能会对其他空间要求执行一百万其他操作。

      要完成其他答案,您可以使用ghci中的
      :sprint
      命令检查惰性评估的工作方式:

      Prelude> let xs = [1..10] :: [Int]
      Prelude> :sprint xs
      xs = _
      Prelude> head xs
      1
      Prelude> :sprint xs
      xs = 1 : _
      Prelude> take 3 xs
      [1,2,3]
      Prelude> :sprint xs
      xs = 1 : 2 : 3 : _
      Prelude> length xs
      10
      Prelude> :sprint xs
      xs = [1,2,3,4,5,6,7,8,9,10]
      

      我不明白你在问什么。看起来您试图对表达式的求值进行推理,但我不知道您在示例中所说的是什么。在谷歌了解更多信息的关键词是“WHNF”。如果有人试图直接回答你的问题,那么答案将是:“在你的例子中,没有一个表达式被计算过”。你的意思是
      head[1,2]
      直接编译为
      1
      吗?这个问题没有任何意义,因为在强制执行值之前(例如,您的代码使用该值执行IO),没有任何计算结果。什么
      main=print$head[1..]
      将被编译二是使用的优化问题,它也可能被编译为“print 1”。此外,对诸如
      [a..b]
      列表之类的内置结构进行推理也有点困难。如果你