-
Notifications
You must be signed in to change notification settings - Fork 11
/
Sum.hs
220 lines (167 loc) · 5.57 KB
/
Sum.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
{-
---
fulltitle: "Optional exercise: foldr vs. foldl"
date: September 6, 2023
---
This module contains some quick examples demonstrating the difference between
foldr and foldl. It is advanced material for CIS 5520, designed for those who
have seen `fold` and tail recursion before, such as in CIS 1200.
-}
module Sum where
import Prelude hiding (foldl, foldr)
{-
Let's start with a concrete example of a fold --- the "sum" function that adds
together all numbers in a list. We can write this function in four different
ways. The first two are the standard recursive definitions of sum. The third
and fourth use a helper function that includes an accumulator.
-}
sum1 :: [Int] -> Int
sum1 [] = 0
sum1 (x : xs) = x + sum1 xs
sum2 :: [Int] -> Int
sum2 [] = 0
sum2 (x : xs) = sum2 xs + x
sum3 :: [Int] -> Int
sum3 = sumAux 0
where
sumAux acc [] = acc
sumAux acc (x : xs) = sumAux (acc + x) xs
sum4 :: [Int] -> Int
sum4 = sumAux 0
where
sumAux acc [] = acc
sumAux acc (x : xs) = sumAux (x + acc) xs
{-
All of these functions give us the same result because (+) is associative and
commutative. However, none of these functions give us exactly the same
*computation*: they each process the list in a different order.
sum1 [1,2,3]
== 1 + (2 + (3 + 0))
sum2 [1,2,3]
== ((0 + 3) + 2) + 1
sum3 [1,2,3]
== ((0 + 1) + 2) + 3
sum4 [1,2,3]
== (3 + (2 + (1 + 0)))
Generalizing Fold
------------------
We can generalize the examples above to create several different
recursion patterns over lists. Compare these definitions with
the variants of `sum` above. Then try to write the missing fourth
variant`.
-}
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f b = go
where
go [] = b
go (x : xs) = x `f` go xs
foldrFlip :: (b -> a -> b) -> b -> [a] -> b
foldrFlip f b = go
where
go [] = b
go (x : xs) = go xs `f` x
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl f = go
where
go acc [] = acc
go acc (x : xs) = go (acc `f` x) xs
foldlFlip :: (a -> b -> b) -> b -> [a] -> b
-- >>> foldrFlip (flip (:)) [] [1,2,3]
-- >>> foldl (flip (:)) [] [1,2,3]
-- >>> foldlFlip (:) [] [1,2,3]
{-
On the other hand, the `(-)` operator is not associative,
so again the results will be different.
-}
-- >>> foldr (-) 0 [1,2,3]
-- >>> foldrFlip (-) 0 [1,2,3]
-- >>> foldl (-) 0 [1,2,3]
-- >>> foldlFlip (-) 0 [1,2,3]
{-
Tail Recursion
--------------
Somewhat surprisingly, the definitions of `sum3` and `sum4` are *not* tail recursive. The
problem is due to laziness: the argument in the recursive call to `sumAux`
is not evaluated until the result is needed. To get an actual tail recursive function
in Haskell, we need to evaluate this accumulator before `sumAux` is called recursively.
Therefore, we will redefine `sum4` using the operation `($!)` which overrides laziness
and forces call-by-value function application. In otherwords, with this operator, GHC
will compile the code to evaluate the argument before making a recursiove call.
-}
sum5 :: [Int] -> Int
sum5 = sumAux 0
where
sumAux acc [] = acc
sumAux acc (x : xs) = (sumAux $! (x + acc)) xs
{-
We can generalize this pattern by adding a strictness annotation to the definition
of foldl. This is the definition of `foldl'` in the standard library.
-}
foldl' :: (b -> a -> b) -> b -> [a] -> b
foldl' f = go
where
go acc [] = acc
go acc (x : xs) = (go $! acc `f` x) xs
{-
Microbenchmarks
----------------
Here are some micro-benchmarks for thinking about laziness and saved
computation.
If you would like to better understand the performance of various folds,
you can use the `:set +s` command in GHCi to get timing and allocation
information for each evaluation that you do.
To do so, first load the definitions in this module into GHCi. To do so,
start the terminal in VS Code, and then use the command
stack ghci Sum.hs
to start ghci and load the module.
Next, in GHCi you can type
Sum> :set +s
to cause GHCi to report timing and allocation data. If you make changes
to any of the definitions in this file, you will need to reload it in
ghci using the command
Sum> :r
For example, to compare the performance of the sum functions we can
call them with large lists:
-}
c1, c2, c3, c4, c5 :: Int
c1 = sum1 [1 .. 1000000]
c2 = sum2 [1 .. 1000000]
c3 = sum3 [1 .. 1000000]
c4 = sum4 [1 .. 1000000]
c5 = sum5 [1 .. 1000000]
{-
When you ask GHCi to evaluate each of these computations, you will see
both the timing and the number of bytes allocated.
ghci> c1
500000500000
(0.38 secs, 226,778,360 bytes)
However, remember that GHC is lazy, so it will save the result. If you
ask for the same value again, it will take much less time and space.
ghci> c1
500000500000
(0.00 secs, 315,144 bytes)
Here are some other examples to try. What can you learn about Haskell's execution
model from these examples?
-}
-- A potentially big computation
h1 :: Int -> Int
h1 y = sum1 [1 .. y * 100000]
-- A potentially big computation (ignore first argument)
f1 :: Int -> Int -> Int
f1 _x y = sum1 [1 .. y * 100000]
-- A potentially big computation (don't ignore first argument)
g1 :: Int -> Int -> Int
g1 x y = if x > 0 then sum1 [1 .. y * 100000] else sum2 [1 .. y * 100000]
-- Call h1 with same argument multiple times.
u1 :: Int
u1 = h1 1 + h1 1 + h1 1
-- Call f1 with same and different arguments multiple times.
v1 :: Int
v1 = f1 0 1 + f1 1 1 + f1 2 1
v2 :: Int
v2 = f1 0 1 + f1 0 1 + f1 0 1
-- Call g1 with same and different arguments multiple times.
w1 :: Int
w1 = g1 0 1 + g1 1 1 + g1 2 1
w2 :: Int
w2 = g1 0 1 + g1 0 1 + g1 0 1