首页 > 解决方案 > 具有头部功能的 Haskell 懒惰问题

问题描述

信息:

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

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

现在我知道,如果我写:

filter odd [1, 2, 3]

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

我对此有几个问题:


函数是head惰性的吗?

如果不是,为什么它不懒惰?

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

我给你举个例子:

f a b = head a + head b

f [2, 3] [4, 5]

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

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

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

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

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

最后:

f head [1, 2]

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

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

谢谢

标签: haskell

解决方案


我认为这里有些混乱,因为“懒惰”一词有时会在两种不同的情况下使用。

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

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

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

当上述评估时,结果是什么?

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

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

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

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

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

例如,我们if bottom then .. else ..总是会产生底部。类似地bottom + 4,它是底部。再次,case bottom of SomeConstructor -> ...; ...是底部(好吧,除了newtypes,但让我们忽略这一点)。取而代之f bottom的可能是也可能不是底部,具体取决于做什么f:如果它需要参数,则结果将是底部。

关于惰性(非严格)函数。一个函数f有时被称为“惰性的”(或者,更准确地说,是非严格的)当且仅当且仅当f bottom是底部。

例如:

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 bottomcase bottom of ...底,head就不懒了。在操作上,由于head在产生结果之前需要它的参数,所以它是严格/非懒惰的。

About g:惰性语义的一个主要特征是data像对构造函数这样的构造(,)函数本质上是惰性的。这(bottom, 4)与底部不同:这使得snd (bottom, 4) = 4即使第一对组件是“错误”值也可以具有。

所以,g bottom = (bottom, True)不是底部,我们可以在不触发错误的情况下申请snd提取。True


推荐阅读