Haskell中的函数是否总是评估其返回值?
我试图更好地理解Haskell的懒惰,比如何时评估一个函数的参数。
来自此源:
但是当对const
一个调用进行评估时(这就是我们感兴趣的情况,毕竟在这里),它的返回值也被评估......这是一个很好的总体原则:一个函数显然是严格的返回值,因为当需要评估函数应用程序时,它需要在函数的主体中评估返回的内容。 从那里开始,您可以通过查看返回值总是取决于什么来了解必须评估的内容。 你的功能在这些论据中将是严格的,在其他论文中是懒惰的。
那么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? 如果不计算计算product [1..]
如何bar
在x
中严格执行?
首先,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机器。
我总是发现,我在其他语境(例如Scheme编程)中学到的术语“评估”在我试图将其应用于Haskell时总是让我感到困惑,而当我开始思考Haskell在强迫表达式而不是“评估”它们。 一些关键差异:
在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 *
我相信这个想法必须是一个懒惰的语言,如果你正在评估一个函数应用程序,那肯定是因为你需要应用程序的结果。 因此无论原因是什么原因导致功能应用程序首先被减少,将继续需要减少返回的结果。 如果我们不需要函数的结果,我们不会首先评估这个调用,那么整个应用程序将被视为一个thunk。
关键的一点是,标准的“懒惰评估”顺序是需求驱动的。 你只评估你需要什么。 评估违反语言规范的“非严格语义”定义的更多风险以及某些应该能够终止的程序的循环或失败; 懒评估具有一个有趣的特性,即如果任何评估命令都可以导致特定的程序终止,那么可以懒惰的评估
但是,如果我们只评估我们需要什么,那么“需要”是什么意思? 一般来说,它也意味着
[]
还是_:xs
),但无法知道要在foo
的定义中使用哪个分支) Int
值来调用这些操作) main
IO操作的外部驱动程序需要知道下一步要执行的是什么 所以说我们有这个计划:
foo :: Num a => [a] -> [a]
foo [] = []
foo (_:xs) = map (* 2) xs
main :: IO ()
main = print (head (foo [1..]))
要执行main
,IO驱动程序必须评估thunk print (head (foo [1..]))
以确定print
应用于thunk head (foo [1..])
。 为了打印它, print
需要评估它的参数,所以现在我们需要评估这个thunk。
head
从模式匹配它的参数开始,所以现在我们需要评估foo [1..]
, 但仅限于WHNF - 仅仅足以说明最外层列表构造函数是[]
还是:
。
foo
从其参数的模式匹配开始。 所以我们需要评估[1..]
,也仅限于WHNF。 这基本上是1 : [2..]
,这足以看到哪个分支采取foo
.2
:
foo
( xs
绑定到thunk [2..]
)的情况评估为thunk map (*2) [2..]
。
所以foo
被评估,并没有评估它的身体。 但是,我们只是这样做的,因为head
是模式匹配来查看我们是否有[]
或x : _
。 我们仍然不知道,所以我们必须立即继续评估foo
的结果。
这是文章所说的功能在结果上严格时的意思。 考虑到对foo
的调用被评估,其结果也将被评估(因此,评估结果所需的任何东西也将被评估)。
但是需要评估多少取决于调用上下文。 head
只是对foo
的结果进行模式匹配,所以对于WHNF只需要一个结果。 我们可以给WHNF一个无限的列表(我们已经这样做了,用1 : [2..]
),所以当评估对foo
的调用时,我们不一定会陷入无限循环。 但是,如果head
是某种哈斯克尔是需要传递一个完全评估名单之外实现基本的操作,那么我们会被评估foo [1..]
完全,因此绝不会为了回来完成head
。
所以,为了完成我的例子,我们正在评估map (2 *) [2..]
。
map
模式匹配其第二个参数,所以我们需要评估[2..]
至2 : [3..]
。 这足以让map
返回WHNF中的thunk (2 *) 2 : map (2 *) [3..]
。 所以它做了,我们终于可以回到head
。
head ((2 *) 2 : map (2 *) [3..])
并不需要检查的两边:
,它只需要知道有一个,因此它可以返回左侧。 所以它只是返回未评估的thunk (2 *) 2
。
再次,虽然,我们只评估了呼叫head
,因为到目前为止,这print
需要知道它的结果是什么,所以虽然head
不评价它的结果,其结果总是评价每当调用head
是。
(2 *) 2
计算为4
, print
将其转换为字符串"4"
(通过show
),并将该行打印到输出。 这是整个IO的main
行动,所以程序完成了。
1 Haskell的实现(如GHC)并不总是使用“标准懒惰评估”,而语言规范并不需要它。 如果编译器可以证明总是需要某些东西,或者不能循环/错误,那么即使懒惰评估不(也)这样做,评估它也是安全的。 这通常会更快,因此GHC优化确实可以做到这一点。
2我在这里跳过了一些细节,比如print
有一些非原始的实现,我们可以进入并且懒惰地评估,并且[1..]
可以进一步扩展到实际实现该语法的函数。
上一篇: Does a function in Haskell always evaluate its return value?
下一篇: How is foldl lazy?