我正在努力更好地理解Haskell的懒惰,例如当它评估函数的参数时。
从此source:
但是当评估对
const
的调用时(这是我们感兴趣的情况,毕竟这里),它的返回值也被评估......这是一个很好的一般原则:一个函数显然是其返回值是strict,因为当需要对函数应用程序进行求值时,它需要在函数体中计算返回的内容。从那里开始,您可以通过查看返回值总是依赖于什么来了解必须评估的内容。你的函数在这些参数中是严格的,在其他参数中是懒惰的。
那么Haskell中的函数总是会评估自己的返回值吗?如果我有:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
head (foo [1..]) -- = 4
根据以上段落,map (* 2) xs
必须进行评估。直觉上,我认为这意味着将map
应用于整个列表 - 导致无限循环。
但是,我可以成功地取得结果。我知道:
在Haskell中是懒惰的,所以这是否意味着评估map (* 2) xs
只意味着构建尚未完全评估的其他东西?
评估应用于无限列表的函数意味着什么?如果在评估函数时总是评估函数的返回值,那么函数是否可以实际返回一个thunk?
编辑:
bar x y = x
var = bar (product [1..]) 1
此代码不会挂起。当我创建var
时,它不评估它的身体吗?或者是否将bar
设置为product [1..]
而不评估?如果后者,bar
没有在WHNF返回它的身体,那么它真的'评估'x吗?如果bar
x
如果不挂在计算product [1..]
上,{{1}}怎么可能是严格的?
答案 0 :(得分:12)
首先,Haskell没有说明评估何时发生,所以问题只能给出具体实现的明确答案。
以下情况适用于我所知道的所有非并行实现,例如ghc,hbc,nhc,hugs等(所有G-machine,btw)。
顺便说一句,要记住的是,当你听到Haskell的“评价”时,通常意味着“评估为WHNF”。与严格语言不同,您必须区分函数的两个“调用者”,第一个是词法调用发生的位置,第二个是需要值的位置。对于严格的语言,这两者总是一致的,但不适合懒惰的语言。 让我们举个例子,让它复杂一点:
foo [] = []
foo (_:xs) = map (* 2) xs
bar x = (foo [1..], x)
main = print (head (fst (bar 42)))
foo
功能发生在bar
中。评估bar
将返回一对,并且该对的第一个组件是与foo [1..]
对应的thunk。所以bar
是严格语言中的调用者,但是对于懒惰语言,它根本不调用foo
,而只是构建闭包。
现在,在main
函数中,我们实际上需要head (fst (bar 42))
的值,因为我们必须打印它。因此实际上将调用head
函数。 head
函数由模式匹配定义,因此它需要参数的值。所以调用了fst
。它也是由模式匹配定义的,需要它的参数,因此调用bar
,bar
将返回一对,fst
将评估并返回其第一个组件。现在终于foo
被“召唤”了;并且通过调用我的意思是thunk被评估(因为它有时在TIM术语中被称为输入),因为需要该值。调用foo
的实际代码的唯一原因是我们想要一个值。所以foo
最好返回一个值(即WHNF)。 foo
函数将评估其参数并最终进入第二个分支。在这里,它将调用map
的代码。 map
函数由模式匹配定义,它将评估其参数,这是一个缺点。因此map将返回以下{(*2) y} : {map (*2) ys}
,其中我使用{}
来表示正在构建的闭包。因此,您可以看到map
只返回一个cons单元格,其头部是闭包,尾部是闭包。
为了更好地理解Haskell的操作语义,我建议你看一些描述如何将Haskell转换为一些抽象机器的文章,比如G-machine。
答案 1 :(得分:3)
我总是发现术语"评估,"我在其他环境中学到的东西(例如,Scheme编程),当我试图将它应用到Haskell时总是让我感到困惑,当我开始考虑强制>时,我突然想到了Haskell strong>表达式而不是"评估"他们。一些关键的区别:
在Haskell中,某些属性"有一个不友好的名字弱头普通形式(" WHNF"),这实际上只是表达式是一个无效的数据构造函数或数据构造函数的应用程序。
让我们将其转化为一套非常粗略的非正式规则。强制表达expr
:
expr
是一个无效的构造函数或构造函数应用程序,则强制它的结果是expr
本身。 (它已经在WHNF了。)expr
是函数应用程序f arg
,则以这种方式获取强制它的结果:
f
。arg
进行模式匹配吗?如果没有,请强制arg
并再次尝试其结果。f
正文中的模式匹配变量替换为与其对应的(可能已重写的)arg
部分,并强制生成表达式。一种思考方式是,当你强制使用表达式时,你会尝试将其重写为最低限度,以将其减少为WHNF中的等效表达式。
我们将此应用于您的示例:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
-- We want to force this expression:
head (foo [1..])
我们需要head
和`map:
head [] = undefined
head (x:_) = x
map _ [] = []
map f (x:xs) = f x : map f x
-- Not real code, but a rule we'll be using for forcing infinite ranges.
[n..] ==> n : [(n+1)..]
现在:
head (foo [1..]) ==> head (map (*2) [1..]) -- using the definition of foo
==> head (map (*2) (1 : [2..])) -- using the forcing rule for [n..]
==> head (1*2 : map (*2) [2..]) -- using the definition of map
==> 1*2 -- using the definition of head
==> 2 -- using the definition of *
答案 2 :(得分:2)
我认为这个想法必须是懒惰的语言,如果你正在评估一个函数应用程序,那一定是因为你需要应用程序的结果。因此无论什么原因导致函数应用程序首先被减少,将继续需要减少返回的结果。如果我们不需要函数的结果,我们就不会在第一时间评估调用,整个应用程序将被留作thunk。
关键是标准"懒惰的评价"订单是需求驱动的。您只评估您需要的东西。评估更多违反语言规范的定义"非严格语义"和一些应该能够终止的程序的循环或失败;懒惰评估有一个有趣的属性,如果任何评估顺序可以导致特定程序终止,那么懒惰评估。 1
但如果我们只评估我们需要什么,那么"需要"意思?通常它意味着
foo
的定义中要采用什么分支,而不知道参数是[]
还是{ {1}})_:xs
值来调用这些操作)Int
IO操作的外部驱动程序需要知道下一步要执行的操作所以说我们有这个程序:
main
要执行foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
main :: IO ()
main = print (head (foo [1..]))
,IO驱动程序必须评估thunk main
以确定它print (head (foo [1..]))
应用于thunk print
。 head (foo [1..])
需要评估其关于打印它的论点,所以现在我们需要评估那个thunk。
print
从匹配其参数的模式开始,所以现在我们需要评估head
,但仅限于WHNF - 足以判断最外层列表构造函数是否为{ {1}}或foo [1..]
。
[]
从其参数的模式匹配开始。所以我们需要评估:
,也只评估WHNF。这基本上是foo
,这足以看出要在[1..]
中采用哪个分支。 2
1 : [2..]
foo
:
绑定到thunk foo
)的情况评估为thunk xs
。
因此[2..]
被评估,并且没有评估其身体。但是,我们之所以这样做是因为map (*2) [2..]
是模式匹配,以确定我们是foo
还是head
。我们仍然不知道,所以我们必须立即继续评估[]
的结果。
这个是文章所说的功能严格的结果。 鉴于对x : _
的调用进行了评估,其结果也将被评估(因此,评估结果所需的任何内容也将被评估)。
但是需要评估多远取决于调用上下文。 foo
仅对foo
的结果进行模式匹配,因此只需要WHNF的结果。我们可以获得WHNF的无限列表(我们已经使用head
),因此在评估对foo
的调用时,我们不必进入无限循环。但是如果1 : [2..]
是在Haskell之外实现的某种原始操作需要传递一个完全评估的列表,那么我们就会完全评估foo
,因此永远不会完成回到head
。
因此,为了完成我的示例,我们正在评估foo [1..]
。
head
模式匹配其第二个参数,因此我们需要将map (2 *) [2..]
评估为map
。这足以让[2..]
返回WHNF中的thunk 2 : [3..]
。完成后,我们终于可以返回map
。
(2 *) 2 : map (2 *) [3..]
不需要检查head
的任何一方,它只需要知道有一条它可以返回左侧。所以它只返回未评估的thunk head ((2 *) 2 : map (2 *) [3..])
。
我们再次评估了对此:
的调用,因为(2 *) 2
需要知道其结果是什么,所以虽然head
没有评估其结果,但是只要对print
的调用是。
head
评估为head
,(2 *) 2
将其转换为字符串4
(通过print
),并将该行打印到输出。这是整个"4"
IO操作,因此程序已完成。
1 Haskell的实现,例如GHC,并不总是使用"标准延迟评估",语言规范不要求它。如果编译器可以证明总是需要某些东西,或者不能循环/错误,那么即使懒惰评估还没有(但是)这样做,它也可以安全地评估它。这通常可以更快,因此GHC优化确实可以做到这一点。
2 我在这里跳过一些细节,就像show
确实有一些非原始的实现,我们可以进入并懒洋洋地评估,{{1可以进一步扩展到实际实现该语法的函数。
答案 3 :(得分:0)
不一定。 Haskell是懒惰的,这意味着它只在需要时进行评估。这有一些有趣的效果。如果我们采用以下代码,例如:
-- File: lazinessTest.hs
(>?) :: a -> b -> b
a >? b = b
main = (putStrLn "Something") >? (putStrLn "Something else")
这是该计划的输出:
$ ./lazinessTest
Something else
这表示永远不会评估putStrLn "Something"
。但它仍然以'thunk'的形式传递给函数。这些“thunk”是未评估的值,而不是具体的值,就像是如何计算价值的面包屑痕迹。这就是Haskell懒惰的作用。
在我们的例子中,两个'thunks'被传递给>?
,但只传递了一个,这意味着最后只评估了一个。这也适用于const
,其中第二个参数可以安全地被忽略,因此从不计算。至于map
,GHC足够聪明,可以意识到我们并不关心数组的结尾,只是想要计算它需要的东西,在你的情况下是原始列表的第二个元素。
然而,最好把关于懒惰的思考留给编译器并保持编码,除非你正在处理IO,在这种情况下你真的应该考虑懒惰,因为你很容易出错,因为我'我刚刚演示过。
答案 4 :(得分:-2)
函数可以评估返回类型:
head (x:_) = x
或异常/错误:
head _ = error "Head: List is empty!"
或者底部(⊥)
a = a
b = last [1 ..]