Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 976
orome Avatar asked Oct 10 '15 14:10

orome


2 Answers

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.

like image 142
dfeuer Avatar answered Nov 05 '22 13:11

dfeuer


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)
like image 39
Erik Kaplun Avatar answered Nov 05 '22 14:11

Erik Kaplun