I'm confused about the description of how Haskell's seq
works in a tutorial I'm reading.
The tutorial states that
evaluating the expression
seq x y
will first evaluatex
to WHNF and only then continue with the evaluation ofy
But earlier the same tutorial, in explaining how Haskell's lazy evaluation works in general, states that in evaluating a function, argues are "evaluated, but only as far as necessary" which means that its
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.
This description of function evaluation in general, seems no different from the description given for seq
. Both — in my beginner's reading — just reduce their first argument to WHNF.
Is that correct? How is seq
different — specifically in how it processes its first argument — from any other Haskell function?
Without seq
, evaluate
, bang patterns, etc., the following rule applies:
All evaluation is driven directly by pattern matching, evaluation of
if
conditions, or primitive numerical operations.
As it turns out, we can squint a bit and make all of those look like pattern matching:
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
etc. Ultimately, the thing being evaluated at any time is the very next primitive IO
action the program will take, and everything else is just recursively driven by pattern matching.
The left-to-right business means that, for instance,
foo False 'd' = ...
foo True _ = ...
is equivalent to
foo x y = case x of
False -> case y of
'd' -> ...
True -> ...
So if foo
is applied to True
and some other value, it doesn't bother forcing that value because it checks the left pattern first.
seq
, when applied to data, acts just like a silly case
. If x :: Bool
, then
x `seq` y = case x of
True -> y
False -> y
But seq
can, rather shadily, be applied to functions as well, or to things of unknown type. It offers a way to force evaluation of things aside from the usual chain of pattern matching.
In the early days of Haskell, seq
was a method of a Seq
class, and this made good sense. Unfortunately, the implementers found it annoying to have to deal with that class when it was easier to just "cheat" and make seq
work for everything. So they cheated, and certain aspects of program analysis and transformation have been harder ever since.
seq
evaluates its first argument immediately, not only when it's needed — this is not at all the same as the general function evaluation.
For example
let x = 1+1
y = 2+2
in seq x (x, y)
immediately evaluates the expression 1+1
but not 2+2
even though neither would need to be evaluated immediately. Figuratively, what's returned is
(2, 2+2)
not (1+1, 2+2)
.
This is sometimes useful if instead of 1+1
you have something like 1+2+3+...+1000000
which is a relatively cheap computation but its unevaluated, lazy form is very very long and takes up a lot of memory, which, if the expression isn't evaluated quickly enough, will effectively start leaking memory; this situatation is called a space leak in Haskell terminology.
EDIT:
To address your comment, here's a bogus scenario wherein a nested data structure is pattern matched to various depth. The data structure has been interspersed with trace
calls at every level so that you could monitor how it's being evaluated:
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)
Output:
"hello"
---
!PERSON!
"hello"
---
!NAME!
"hello"
---
!AGE!
"hello"
---
hello: !NAME CONTENT!
John Doe
---
hello: ("John Doe",!AGE CONTENT!
28)
Note that the output from the trace
calls "interferes" with the output from the putStrLn
/print
calls, but it actually demonstrates quite well how the evaluation is happening at runtime.
Furthermore, if you define Name
and Age
using newtype
instead of data
, the evaluation will be slightly different as newtype
values don't have a runtime wrapper so the runtime memory representation of person
will be one "level" thinner:
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With