Skip to content

Commit d1432be

Browse files
committed
Complete chapter 27 - Non-Strictness
1 parent a51f3d0 commit d1432be

File tree

3 files changed

+316
-0
lines changed

3 files changed

+316
-0
lines changed

ch-27/README.md

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Non-Strictness
2+
3+
* Truly lazy language memoizes results of `all` functions it ever evaluates
4+
which will consume large amounts of memory for practical programs.
5+
6+
* Non-strictness means language should be able to evaluate expressions with bottoms
7+
in them as long as they (bottoms) are never evaluated.
8+
9+
* Implementations of Haskell are technically required to be `non-strict`
10+
rather than lazy, but for practical purposes lets call it `lazy`.
11+
12+
* Most expressions are only reduced/evaluated when necessary.
13+
Never needed - never evauluated. Unevaluated expressions are called `thunks`.
14+
The garbage collector (GC) sweeps the thunks away if never evaluated.
15+
16+
* So `GHC` is totally non-strict (as required by the specification of the language)
17+
and somewhat lazy (not truly lazy because of the garbage collection).
18+
19+
* `evaluation = thunk -> value`
20+
21+
* `Thunk` is placeholder in the underlying graph of the program.
22+
23+
* If thunk is evaluated, it can often be shared between expressions - this is laziness.
24+
25+
* The idea is that evaluation is driven by `demand`, not by construction.
26+
This also means we can never guarantee is something will ever be evaluated.
27+
28+
#### Outside in evaluation
29+
30+
* Strict languages evaluate `inside-out`, non-strict like Haskell do the opposite -
31+
they evaluate `outside-in`. Evaluation proceeds from outermost parts of expressions
32+
and works inward based on what values are forced.
33+
34+
Example:
35+
36+
```haskell
37+
foldr' k z xs = go xs
38+
where
39+
go [] = z
40+
go (y:ys) = y `k` go ys
41+
42+
c = foldr' const 'z' ['a'..'e'] -- returns 'a'
43+
```
44+
45+
`const` returns first argument without evaluating second,
46+
hence our list tail never gets evaluated - so this `foldr'` will work even on
47+
list with `undefined` in it - as long as it is not the first element.
48+
49+
`const` was in outermost position so it was evaluated first.
50+
51+
* Outside in evaluation is the reason why `length` of list never evaluates the
52+
elements of the list.
53+
54+
## Making Haskell Strict
55+
56+
* There are mechanisms to force evaluation
57+
58+
```haskell
59+
seq :: a -> b -> b
60+
```
61+
62+
* `seq` magically forces evaluation of first argument if and when the
63+
second argument has to be evaluated.
64+
* It creates a graph of evaluations - forcing one exp will force another.
65+
eg.
66+
```haskell
67+
a `seq` b `seq` c `seq d
68+
```
69+
means `c` will be evaluated before `d`, but before that `b` but before that `a`
70+
must be evaluated. So `d -> c -> b -> a`.
71+
* `seq` forces evaluation to weak head normal form (WHNF) - which means it stops at
72+
the first data constructor or lambda.
73+
74+
eg.
75+
76+
```haskell
77+
let x = (,) undefined undefined
78+
x `seq` 1 -- works
79+
let y = \_ -> undefined
80+
y `seq` 2 -- works as well
81+
let z = undefined
82+
z `seq` 3 -- does not work - undefined itself is bottom
83+
```
84+
85+
* `x` and `y` in above example work because bottom is enclosed inside data constructor
86+
or function and haskell does not need to evaluate `inside` unless it is forced to
87+
(which seek does not) - this is `WHNF`.
88+
* `case` also forces data constructor evaluation but not inner values
89+
(depends on nesting in the pattern).
90+
91+
## Call by *
92+
93+
* **Call by value:** args evaluated before entering function,
94+
exps evaluated before bindings - `strict` convention.
95+
96+
* **Call by name:** exps not necessarily evaluated - outside-in evaluation.
97+
98+
* **Call by need:** same as call by name, but evaluated only once.
99+
100+
* GHC oscillates between `by-need` and `by-name` based on optimizations.
101+
It can do this because of `pure` code.
102+
103+
104+
* Sidenote: `:sprint` allows to see thunks in `ghci`. `_` is shown for unevaluated values.
105+
106+
107+
## Sharing evaluations
108+
109+
* When a computation is named, results of evaluating that computation can be shared
110+
between all references to that name without re-evaluating.
111+
112+
* Imp to note sharing based on names not values. So `let x = 1` and `let y = 1`
113+
won't be shared. But in `let x = 1` and then `x + x` value is shared.
114+
115+
* Inlining expressions where they get used prevents sharing - thunks evaluated separately.
116+
117+
* Sharing doesn't work in presence of constraints like typeclasses - since they are
118+
functions in `Core` (GHC's intermediate language) which are waiting to be converted
119+
to concrete types - and unapplied functions are not shareable values.
120+
121+
ie. in `Core`, `Num a => a` is really function `Num a -> a`.
122+
123+
But it works if you give a concrete type.
124+
125+
* In short, polymorphic values maybe evaluated once but still not shared because, underneath,
126+
they continue to be functions awaiting application.
127+
128+
#### Preventing sharing
129+
130+
* We might want to prevent sharing if a large amount of data is there in order to compute a small value.
131+
We can discard the large data while preserving the small result.
132+
133+
* Trick to prevent sharing is turning values into `unit` functions:
134+
135+
```haskell
136+
import Debug.Trace
137+
let f x = (x ()) + (x ())
138+
f (\_ -> trace "hi" 2)
139+
140+
-- hi
141+
-- hi
142+
-- 4
143+
```
144+
145+
* Functions are not shared when there are named arguments but are when they are pointfree. :/
146+
147+
#### Forcing sharing
148+
149+
* Name the result using `let` binding. Names are what make values shareable.
150+
151+
#### Lazy patterns
152+
153+
Add tilde `~` before a pattern to make it lazy.
154+
155+
```haskell
156+
strictPattern :: (a, b) -> String
157+
strictPattern (a, b) = const "hello" a
158+
159+
lazyPattern :: (a, b) -> String
160+
lazyPattern ~(a, b) = const "hello" a
161+
162+
strictPattern undefined -- error
163+
164+
lazyPattern undefined -- no error
165+
-- "hello" returned
166+
```
167+
168+
* `strictPattern undefined` above^ worked because we never needed `a` so pattern was never evaluated.
169+
170+
* Caveat is that lazy pattern cannot be used to discriminate cases of a sum.
171+
172+
#### Bang patterns
173+
174+
* Evaluate argument to function whether we use it or not
175+
176+
```haskell
177+
f :: Int -> Int
178+
f !x = 1
179+
```
180+
181+
* Forcing something (with either `seq` or `!`) is expressed in `Core` as `case` expressions.
182+
183+
* We can specify bang pattern in data declarations.
184+
185+
```haskell
186+
type Name = String
187+
type Age = Int
188+
data Person = Person !Name Age
189+
190+
age (Person _ a) = a
191+
192+
age (Person undefined 20) -- fails even if first argument is not used
193+
```
194+
195+
* Idea here is sometimes its cheaper to just compute something than to construct thunk and then evaluate later.
196+
197+
## Strict/StrictData pragmas
198+
199+
* Avoid putting `seq` and `!` yourself. Makes everything strict in that module.
200+
201+
* Can use `~` to bring back laziness.

ch-27/non-strictness.hs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
3+
foldr' k z xs = go xs
4+
where
5+
go [] = z
6+
go (y:ys) = y `k` go ys
7+
8+
hypo :: IO ()
9+
hypo = do
10+
let x :: Int
11+
x = undefined -- in non-strict langs, this will be forced before binding
12+
s <- getLine
13+
case s of
14+
"hi" -> print x
15+
_ -> putStrLn "hello"
16+
-- but in non-strict langs like haskell x is bound but not forced evaluation
17+
-- until required - hence it works as long as input is not "hi"
18+
19+
-- Strict version of above function
20+
hypo' :: IO ()
21+
hypo' = do
22+
let x :: Int
23+
x = undefined
24+
s <- getLine
25+
case x `seq` s of
26+
"hi" -> print x
27+
_ -> putStrLn "hello"
28+
-- `seq forces evaluation of first arg when second is evaluated.
29+
-- Since `s` is evaluated every time - no matter what you enter now -
30+
-- `x` will also be evaluated leading the program to fail.
31+
-- Still it really is not truly a strict lang because getLine was evaluated before `x`
32+
33+
hypo'' :: IO ()
34+
hypo'' = do
35+
let x :: Int
36+
x = undefined
37+
s <- x `seq` getLine -- force x before getLine
38+
-- This^ is what real strict languages would do
39+
case s of
40+
_ -> putStrLn "doesn't matter"
41+
42+
notGonnaHappenBru :: Int
43+
notGonnaHappenBru =
44+
let x = undefined
45+
y = 2
46+
z = (x `seq` y `seq` 10, 11)
47+
in snd z
48+
-- snd returns 11 - so fst never evaluated
49+
50+
doesntEval :: Bool -> Int
51+
doesntEval b = 1
52+
53+
manualSeq :: Bool -> Int
54+
manualSeq b = b `seq` 1
55+
56+
banging :: Bool -> Int
57+
banging !b = 1
58+
59+
-- Making lazy lists strict
60+
data List a =
61+
Nil
62+
| Cons !a !(List a)
63+
deriving Show
64+
65+
sTake :: Int -> List a -> List a
66+
sTake n _
67+
| n <= 0 = Nil
68+
sTake n Nil = Nil
69+
sTake n (Cons a xs) = Cons a $ sTake (n - 1) xs
70+
71+
twoEls = Cons 1 (Cons undefined Nil)
72+
oneEl = sTake 1 twoEls
73+
threeEls = Cons 2 twoEls
74+
oneElT = sTake 1 threeEls
75+
76+
-- Chapter Exercise
77+
-- Make expression bottom - using only bang patterns or seq
78+
x = undefined
79+
y = "blah"
80+
main = do
81+
print $ snd (x, x `seq` y)

ch-27/strict.hs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{-# LANGUAGE Strict #-} -- Make entire module strict by default
2+
3+
module StrictTest where
4+
5+
blah x = 1
6+
7+
-- everything is Strict
8+
-- this fails even if function does not use arg
9+
blahTest :: IO ()
10+
blahTest = print (blah undefined)
11+
12+
-- bring back laziness if needed
13+
willNotForce :: Int -> Int
14+
willNotForce ~x = 1
15+
-- passing undefined to this func will work - even within Strict module
16+
17+
-- Chapter Exercises
18+
data List a =
19+
Nil
20+
| Cons ~a ~(List a)
21+
deriving (Show)
22+
23+
take' n _ | n <= 0 = Nil
24+
take' _ Nil = Nil
25+
take' n (Cons x xs) = Cons x $ take' (n - 1) xs
26+
27+
map' _ Nil = Nil
28+
map' f (Cons x xs) = Cons (f x) $ map' f xs
29+
30+
repeat' x = xs where xs = (Cons x xs)
31+
32+
main = do
33+
print $ take' 10 $ map' (+1) (repeat' 1)
34+
-- adding ~ here makes the main work otherwise it goes in infinite loop with strict repeat

0 commit comments

Comments
 (0)