Haskell 的 'seq' 与其他函数有何不同?

How is Haskell's 'seq' different from other functions?

我对 Haskell 的 seqtutorial I'm reading 中如何工作的描述感到困惑。

教程指出

evaluating the expression seq x y will first evaluate x to WHNF and only then continue with the evaluation of y

但在同一教程的前面,在解释 Haskell 的惰性求值一般如何工作时,指出在求值函数时,争论的是 "evaluated, but only as far as necessary" 这意味着它的

arguments will be evaluated from left to right until their topmost nodes are constructors that match the pattern. If the pattern is a simple variable, then the argument is not evaluated; if the pattern is a constructor, this means evaluation to WHNF.

这种对一般函数求值的描述,似乎与 seq 的描述没有什么不同。在我的初学者读物中,两者都只是将他们的第一个参数简化为 WHNF。

对吗? seq 与任何其他 Haskell 函数有何不同——特别是它处理第一个参数的方式?

如果没有 seqevaluate、刘海图案等,则适用以下规则:

All evaluation is driven directly by pattern matching, evaluation of if conditions, or primitive numerical operations.

事实证明,我们可以稍微眯起眼睛,让 所有 看起来像模式匹配:

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的早期,seqSeqclass的一个方法,这是有道理的。不幸的是,实施者发现不得不处理 class 很烦人,而只 "cheat" 并使 seq 适用于所有内容更容易。所以他们作弊了,从那以后程序分析和转换的某些方面变得更加困难。

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 的东西,这有时很有用,这是一个相对便宜的计算,但它的未评估的惰性形式非常非常长并且占用大量内存,如果表达式的计算速度不够快,将会有效地开始泄漏内存;这种情况在 Haskell 术语中称为 space 泄漏


编辑:

为了解决您的评论,这里有一个虚假的场景,其中嵌套数据结构与不同深度进行模式匹配。数据结构在每个级别都散布着 trace 调用,以便您可以监控它的评估方式:

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 调用 "interferes" 的输出与 putStrLn/print 调用的输出相同,但它实际上演示了那么评估在运行时是如何发生的。

此外,如果你定义NameAge时使用newtype而不是data,求值会略微不同之处在于 newtype 值没有运行时包装器,因此 person 的运行时内存表示将是一个 "level" 更薄:

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)