【问题标题】:Haskell laziness question with head function具有头部功能的 Haskell 懒惰问题
【发布时间】:2018-10-10 04:58:06
【问题描述】:

信息:

从文档和教程中它说:默认情况下“Haskell 是懒惰的”。

他们没有解释它的细节,我想知道。

现在我知道,如果我写:

filter odd [1, 2, 3]

在结果显示或在表达式中使用之前,它不会过滤结果。

我对此有几个问题:


head 函数是惰性的吗?

如果不是,为什么不偷懒?

如果是惰性的,Haskell 编译器如何知道何时执行函数?

我举个例子:

f a b = head a + head b

f [2, 3] [4, 5]

在这种情况下,从我的角度来看,头部将返回 2 + 4。

它将返回一些类型或函数,稍后在需要时执行。 (如果我在某个地方弄错了,请纠正我)。

所以我的建议是,当 Haskell 看到像 '+' 这样的操作时,它会计算结果。

但它变得更加复杂,因为对于整数我想如果我写 3 + 5 它也可以是惰性表达式。

我怀疑当惰性表达式开始计算时是否存在函数列表,因为很难编写所有变体。

最后:

f head [1, 2]

假设在f 函数中,我打印传递的变量的类型。

现在 Haskell 将如何知道应该传递 Int 1 还是惰性表达式?

谢谢

【问题讨论】:

    标签: haskell


    【解决方案1】:

    我认为这里有些混乱,因为“懒惰”一词有时用于两种不同的上下文。

    • 惰性语义(相对于渴望语义)
    • 惰性函数(实际上应该称为非严格函数)

    关于惰性与渴望语义:考虑这个表达式

    (\x -> 42) (error "urk!")
    

    上面的求值,结果是什么?

    根据 eager 语义,我们在调用函数之前评估参数。结果将是运行时错误。

    根据惰性语义,我们立即调用函数。这个过程可以从操作上和外延上理解如下。

    在操作上,它被传递一个 thunk,一个描述尚未评估的参数的对象,并且每当需要参数 x 时,它将被“强制”(评估)。我们可以假设x 指向未求值的表达式error "urk!",它会在需要x 时求值。

    在表示上,我们使用了一个数学技巧:我们用一个称为“底部”的特殊值来表示错误,并说error "urk!" 具有这样的底部值。 然后,我们简单地假设这个异常值可以被传递。在上面的函数调用中,x 将绑定到“底部”,就好像它是一个正常值一样。这可以说更简单,因为我们不需要关注表达式是如何计算的,而只需要关注底部是如何传播的。

    更准确地说,我们让“底部”表示运行时错误和非终止(无限递归),这两者都让程序能够返回实际结果。

    例如,我们有if bottom then .. else .. 将始终产生底部。对于底部的bottom + 4 也是如此。同样,case bottom of SomeConstructor -> ...; ... 是底部(好吧,除了newtypes,但让我们忽略这一点)。相反,f bottom 根据f 所做的事情可能会或可能不会是底部:如果它需要参数,结果将是底部。

    关于惰性(非严格)函数。如果 f bottom 是底部函数,则函数 f 有时被称为“惰性”(或者,更准确地说,是非严格函数)。

    例如:

    f x = x+1  -- strict / non lazy
    f x = 5    -- non strict / lazy
    head xs = case xs of   -- strict / non lazy
       [] -> error "head: empty list"
       (x:xs) -> x
    g x = (x,True)   -- non strict / lazy
    

    所以,由于head bottom 是底部的case bottom of ...head 并不懒惰。在操作上,由于head 在产生结果之前要求它的参数,它是严格/非懒惰的。

    关于g:惰性语义的一个主要特点是data 构造函数,如对构造函数(,),本质上是惰性的。那就是(bottom, 4) 与底部不同:这使得snd (bottom, 4) = 4 成为可能,即使第一对组件是“错误”值。

    所以,g bottom = (bottom, True) 不是底部,我们可以应用snd 提取True 而不会触发错误。

    【讨论】:

      【解决方案2】:

      head 函数是惰性的吗?

      是的,Haskell 默认是惰性求值的。

      如果是惰性的,Haskell 编译器如何知道何时执行函数?

      将在需要该值时评估该函数 - 据我了解,这最终会在您以某种方式涉及 IO 时发生。

      在这种情况下,从我的角度来看,head 不会返回 2 + 4。

      正确,返回“值”是所谓的 thunk,它是尚未计算的表达式的另一个名称。是什么表情?我是head a + head b

      惰性求值规则的主要例外是IO,它被热切求值。因此,如果您想打印调用f [2, 3] [4, 5] 的结果,那么足够的表达式被求值以产生要打印的结果。

      如果需要,有一些方法可以提前进行强制评估,例如通过使用seq。这有时很重要,因为这些重击会变得很大。

      【讨论】:

      • 谢谢,这意味着即使语句是惰性的,因为如果比较函数是惰性的,那么 ifs 也应该是惰性的,对吧?
      • 假设函数 f 返回一个普通数字(如 Int),它实际上不会返回 6。它不会,重复 NOT,返回一个 thunk。你问这与 Haskell 懒惰(实际上,非严格)的押韵如何?好吧,事情是惰性将确保在您需要该值之前不会调用 f 。所以返回一个 thunk 是没有意义的,因为它必须立即被强制。
      • @user2693928 我相信你会发现,当人们开始在我的答案上堆砌 cmets 时,GHC 如何完成所有这些工作的实际实现是复杂的。不过,据我了解,比较和分支也会被延迟评估。
      • 谢谢,将答案标记为正确,如果可能的话,只是最后的澄清。如果它在某个采用 Int 的函数中传递了一些惰性类型,那怎么可能。它接受 Int 但我通过了 LazyType(无论 Haskell 中的实现是什么)
      • 对于您的“最终”这句话,我们可以得到更精确的:main,并且只有main,驱动所有评估。 (仅仅涉及 IO 是不够的。x = print (head []); main = return () 不会爆炸,即使 print (head []) 是一个 IO 动作,如果它实际上包含在 main 的执行中就会爆炸。)
      最近更新 更多