我对Haskell seq
在tutorial I'm reading中的工作方式的描述感到困惑。
教程说明了
评估表达式
seq x y
将首先评估x
到WHNF,然后继续评估y
但早先在同一个教程中,在解释Haskell的懒惰评估如何起作用时,指出在评估函数时,争论是“评估,但只在必要时”,这意味着它的
参数将从左到右进行计算,直到它们的最顶层节点是与模式匹配的构造函数。如果模式是一个简单变量,则不评估参数;如果模式是构造函数,这意味着对WHNF的评估。
这种功能评估的描述一般来说与seq
的描述没有什么不同。两者 - 在我初学者的阅读中 - 只是减少他们对WHNF的第一个论点。
这是对的吗? seq
如何与任何其他Haskell函数不同 - 特别是它如何处理它的第一个参数?
答案 0 :(得分:10)
如果没有seq
,evaluate
,爆炸模式等,则适用以下规则:
所有评估都是通过模式匹配,
if
条件评估或原始数值运算直接驱动的。
事实证明,我们可以稍微眯一下,使所有看起来像模式匹配:
if c then x else y
=
case c of
True -> x
False -> y
x > y = case x of
0 -> case y of ...
0 + y = y
1 + 1 = 2
等。最终,正在评估的事物是程序将采取的下一个原始IO
动作,其他一切都只是通过模式匹配递归驱动。
从左到右的业务意味着,例如,
foo False 'd' = ...
foo True _ = ...
相当于
foo x y = case x of
False -> case y of
'd' -> ...
True -> ...
因此,如果将foo
应用于True
以及其他一些值,则不会强制该值,因为它会首先检查左侧模式。
seq
,当应用于数据时,就像愚蠢的case
一样。如果是x :: Bool
,那么
x `seq` y = case x of
True -> y
False -> y
但seq
可以,相当狡猾地,也可以应用于函数,或者应用于未知类型的事物。除了通常的模式匹配链之外,它提供了一种强制评估的方法。
在Haskell的早期,seq
是一个Seq
类的方法,这很有道理。不幸的是,实施者发现,当更容易“欺骗”并让seq
为所有事情工作时,必须处理该类是很烦人的。所以他们欺骗了,从那以后,程序分析和转换的某些方面变得更加困难。
答案 1 :(得分:2)
seq
不仅在需要时立即评估其第一个参数 - 这与一般函数评估完全不同。
例如
let x = 1+1
y = 2+2
in seq x (x, y)
立即评估表达式1+1
但不评估2+2
,即使两者都不需要立即评估。形象地说,返回的是
(2, 2+2)
不是(1+1, 2+2)
。
如果1+1
代替1+2+3+...+1000000
而不是trace
这是一个相对便宜的计算但是它没有评估,懒惰的形式非常长并占用大量内存,这有时很有用,如果表达式评估得不够快,将有效地开始泄漏内存;这种情况在Haskell术语中称为空间泄漏。
修改强>
为了解决您的评论,这里是一个虚假的场景,其中嵌套数据结构与各种深度模式匹配。数据结构中穿插了每个级别的import Debug.Trace (trace)
data Age = Age Int
data Name = Name String
data Person = Person Name Age
name = trace "!NAME!" $ Name $ trace "!NAME CONTENT!" $ "John " ++ "Doe"
age = trace "!AGE!" $ Age $ trace "!AGE CONTENT!" $ 10 + 18
person = trace "!PERSON!" $ Person name age
-- person = trace "!PERSON!" $ Person (trace "!n!" name) (trace "!a!" age)
main = do
case person of p -> print "hello"
putStrLn "---"
case person of Person name age -> print "hello"
putStrLn "---"
case person of Person (Name str) age -> print "hello"
putStrLn "---"
case person of Person (Name str) (Age i) -> print "hello"
putStrLn "---"
case person of Person (Name str) (Age i) -> putStrLn $ "hello: " ++ str
putStrLn "---"
case person of Person (Name str) (Age i) -> putStrLn $ "hello: " ++ show (str, i)
个调用,以便您可以监视它的评估方式:
"hello"
---
!PERSON!
"hello"
---
!NAME!
"hello"
---
!AGE!
"hello"
---
hello: !NAME CONTENT!
John Doe
---
hello: ("John Doe",!AGE CONTENT!
28)
<强>输出:强>
trace
请注意,putStrLn
调用的输出“会干扰”print
/ Name
调用的输出,但它实际上很好地展示了评估的发生方式在运行时。
此外,如果您使用Age
而不是newtype
定义data
和newtype
,则评估会与{{1}略有不同值没有运行时包装器,因此person
的运行时内存表示将是一个“级别”更薄:
newtype Age = Age Int
newtype Name = Name String
data Person = Person Name Age
"hello"
---
!PERSON!
"hello"
---
"hello"
---
"hello"
---
hello: !NAME!
!NAME CONTENT!
John Doe
---
hello: ("John Doe",!AGE!
!AGE CONTENT!
28)