7

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 evaluate x to WHNF and only then continue with the evaluation of y

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?

orome
  • 45,163
  • 57
  • 202
  • 418
  • @Heinrich Apfelmus: A question about the [laziness tutorial](https://hackhands.com/lazy-evaluation-works-haskell/). – orome Oct 10 '15 at 14:35
  • Well... one says 'the argument will not be evaluated, unless needed' and the other one specifically says 'this WILL be evaluated to WHNF'. In what way does this sound the same to you? – Cubic Oct 10 '15 at 14:48
  • @Cubic: In every way, of or course; that's why I asked, clearly. (drip, drip) If you read the whole tutorial (with a beginners eyes, thanks), you'll see that "needed" is perhaps the critical but not explicitly elaborated distinction. – orome Oct 10 '15 at 14:52
  • The first sentence fragment of your second quote is arguably ambiguous (does "their topmost nodes" refer to the single topmost node of each argument, or some indefinite quantity of nodes at the top of each argument? In fact, the latter.) But "If the pattern is a simple variable, then the argument is not evaluated" seems pretty explicit to me... – Reid Barton Oct 10 '15 at 15:05
  • @ReidBarton: Yes, I'm learning a lot about how things seem to others. – orome Oct 10 '15 at 15:06
  • 3
    Maybe we just don't understand what your question is about? It's true that `seq` and the text's example of `(&&)` both reduce their first argument to WHNF. However this is not true of all Haskell function in general. For example `one x = 1` does not ever evaluate its argument at all. Similarly `notseq x y = y` does not ever evaluate `x` because doing so is not necessary to produce the value `y`. – Reid Barton Oct 10 '15 at 15:25

2 Answers2

10

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • So is it correct to say (perhaps at the risk of oversimplifying) that the difference is that in general, evaluation happens only to the extent needed to find a pattern match, while `seq` first determines whether its first argument is not `⊥`, which will (generally, aways?) require further evaluation? – orome Oct 10 '15 at 15:31
  • @raxacoricofallapatorius, vaguely, at least. It forces things that it does not pattern match on, and in many cases *cannot* pattern match on. – dfeuer Oct 10 '15 at 15:50
  • Can you expand upon on `primitive numerical operations` ? I don't think `let a = 2 * 3` forces evaluation. – Sibi Oct 10 '15 at 16:40
  • 1
    @Sibi, if `x * y` is forced, that forces `x` and also `y`. – dfeuer Oct 10 '15 at 16:44
2

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)
Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
  • That makes sense, but how does it map to the second `case` statement in [defeuer's answer](http://stackoverflow.com/a/33055462/656912) — where is evaluation of `x` forced there? – orome Oct 10 '15 at 17:26
  • that's because pattern matching forces the evaluation of the first "layer" in the chain of unevaluated chunks. same with `seq` btw. – Erik Kaplun Oct 10 '15 at 18:18
  • So both `case` statements evaluate their first argument? The two examples aren't don't exhibit different behavior with respect to their first arguments? – orome Oct 10 '15 at 18:22
  • I've added a more elaborate example of how pattern matching forces the evaluation of different parts of a nested data structure. – Erik Kaplun Oct 10 '15 at 21:50
  • Does `case x` force evaluation though? – orome Oct 11 '15 at 14:17
  • yes; evaluation of the first level. there's a more scientific term as well but it doesn't matter here. – Erik Kaplun Oct 11 '15 at 14:34
  • But `g x y = case x of otherwise -> y` does not evaluate `x` (at least not according to `trace`). – orome Oct 11 '15 at 14:49
  • `case (trace "x" x) of foo -> y`? – Erik Kaplun Oct 11 '15 at 15:45
  • Yes, but not with `otherwise`. It's the matching, within the `case`, not the `case` itself. So the `case` examples aren't distinguishing evaluation of the first argument: both do. – orome Oct 11 '15 at 15:54