This repository has been archived by the owner on Apr 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Lec01.hs
410 lines (303 loc) · 15.5 KB
/
Lec01.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
module Lec01 where
{- LECTURE 01 : PROGRAMS MADE OF EQUATIONS; RUNNING THEM
Welcome to the first lecture of the University of Strathclyde's
"Functional Programming" course!
Most of the lectures will be delivered by live Haskell coding on
the projector. We take the final version of the code and annotate
it with comments after the lecture to help you understand what is
going on, and to recreate some of the things we said during the
lecture.
This first lecture introduces the two main concepts in Haskell:
1. Defining what data is.
2. Transforming data by pattern matching.
We will cover both of these in a lot more depth later in the
course. This lecture is a first introduction to the concepts. We
will also introduce the concept of 'type' and how it relates to
these concepts.
In the lecture we introduced the "Evaluation Game" that allows us
to interactively see how pattern matching is used to run
programs. This is the first exercise for this course. See the main
page for instructions on how to access it. -}
{- PART 1 : DEFINING DATA
In Haskell, pieces of data always start with capital letters, or
are numbers, characters, or strings. Pieces of data that start with
a capital letter are called 'constructors'.
Examples: 'Nil', or 'True', or 'False', or '1234'.
Before we can use any old capitalised word as a piece of data, we
need to tell Haskell which words we want to use, and how they are
grouped into datatypes. For example, 'True' and 'False' are grouped
into the 'Bool' type.
We'll introduce this by an example. Here is a Haskell 'data'
declaration that defines a new type 'List' that contains two
constructors 'Nil' and 'Cons': -}
data List a = Nil | Cons a (List a)
deriving Show
{- In English, we read this declaration as:
A 'List' of 'a's is either:
- 'Nil'; or
- 'Cons', followed by an 'a' and a 'List' of 'a's.
The symbol '|' is read as 'or'. So another way to read this
declaration is: "a List is either Nil or a Cons".
Note that the type 'a' can stand for any other type. So we can have
lists containing data of any type.
NOTE: the 'deriving Show' is an instruction to the Haskell
compiler to generate a function for converting 'List's to
strings. Think of this as auto-generating something similar to
Java's 'toString()' methods.
NOTE: The names 'Nil' and 'Cons' to stand for the empty list and
a list with one extra element originally come from the Lisp
programming language, one of the oldest programming
languages. 'Cons' is short for 'Construct'. For historical
interest, see the original paper on Lisp by John McCarthy:
John McCarthy: Recursive Functions of Symbolic Expressions and
Their Computation by Machine, Part I. Commun. ACM 3(4):
184-195 (1960)
PDF at: http://www-formal.stanford.edu/jmc/recursive.pdf
Now lets see some examples of data of type 'List a' for some 'a's,
and how we can deduce that they are actually lists.
Here is our first example: -}
ex1 :: List Int
ex1 = Cons 2 (Cons 7 (Cons 1 Nil))
{- Haskell syntax: this definition consists of two lines:
- The first line names the thing we are defining, 'ex1', and gives
its type 'List Int' (meaning "List of Ints"). The symbol '::' is
read as "has type".
- The second line names the definition again, and the text to the
right of the 'equals' is what we are defining 'ex1' to be.
So, altogether, we are defining 'ex1', of type 'List Int', to be
'Cons 2 (Cons 7 (Cons 1 Nil))'.
In Haskell, we must always make sure that the definition we give
matches the type. In the lecture, we worked through on the board
how we can see that 'ex1' is of type 'List Int'. Let's go through
it here:
GOAL: 'Cons 2 (Cons 7 (Cons 1 Nil)) :: List Int'
From what we said above, a list is either 'Nil' or a 'Cons'. In
this case we have a 'Cons' and it must be followed by an 'Int'
and a 'List Int'. So we check:
* '2' is an 'Int' -- Yes!
* 'Cons 7 (Cons 1 Nil)' is a 'List Int', this is a 'Cons', so we
check again:
- '7' is an 'Int' -- Yes!
- 'Cons 1 Nil' is a 'List Int', so we check:
+ '1' is an 'Int' -- Yes!
+ 'Nil' is a 'List Int' -- Yes! because 'Nil' is one of the
constructors of 'List a', for any 'a'.
So we can now confidently say that 'Cons 2 (Cons 7 (Cons 1 Nil))'
is of type 'List Int'. Of course, the Haskell compiler will check
this for us, but it is useful to know what is going on when it does
this check.
If we load this file into the GHCi interactive Haskell REPL, we can
see that it succeeds:
GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help
Prelude> :l Lec01.hs
[1 of 1] Compiling Lec01 ( Lec01.hs, interpreted )
Ok, modules loaded: Lec01.
*Lec01>
Let's now see what happens when we try something that is not a
valid list:
ex2 :: List ??
ex2 = Cons Nil (Cons 1 Nil)
In this example (which is commented out so that you can load this
file into GHCi for experimentation), we have not even been able to
fill in the '??'. If we go through the same process as before for
checking whether 'ex2' is a list we end up in a situation where
'Nil' and '1' must have the same type. However, 'Nil' is a 'List a'
(for some 'a'), and '1' is a number. If we try to get GHCi to
accept our definition, it returns an error message:
*Lec01> :l Lec01.hs
[1 of 1] Compiling Lec01 ( Lec01.hs, interpreted )
Lec01.hs:160:22:
No instance for (Num (List a0)) arising from the literal ‘1’
In the first argument of ‘Cons’, namely ‘1’
In the second argument of ‘Cons’, namely ‘(Cons 1 Nil)’
In the expression: Cons Nil (Cons 1 Nil)
Failed, modules loaded: none.
This error gives a lot of information, but in essence it is saying
that Haskell cannot work out how to treat lists as numbers.
Our final example shows that it is possible to have lists of lists,
and that it is possible to get Haskell to infer types for us in
many situations.
Here, we just give a definition without a type. -}
ex3 = Cons Nil (Cons Nil Nil)
{- In general, this is bad style. It is very helpful for readers to be
able to see the types of every definition in a Haskell program, as
an aid to understanding. In this course, we will be careful to give
types for all our definitions.
However, it can be useful during development to leave off some of
the types so that we can find out what Haskell thinks the types
ought to be. We can do this by loading the file into GHCi and
making a type query using the special command ':t' (short for
':type'):
Prelude> :l Lec01.hs
[1 of 1] Compiling Lec01 ( Lec01.hs, interpreted )
Ok, modules loaded: Lec01.
*Lec01> :t ex3
ex3 :: List (List a)
*Lec01>
GHCi has told us that the type of 'ex3' is 'List (List a)' -- it is
a list of lists of 'a's, for any type 'a'. -}
{- Haskell already has a built-in list type so we don't need to define
our own every time we want to use lists. Lists are used so often in
Haskell (possibly too much) that they get their own special syntax.
Instead of writing the type 'List a', the built-in type is written
'[a]', read as "list of 'a's".
Instead of writing 'Nil', the empty list is written '[]'.
Instead of writing 'Cons x xs', we write 'x : xs'. This is spoken
as 'x cons xs'.
It is possible to write lists using ':' and '[]', for example: -}
ex1' :: [Int]
ex1' = 2 : 7 : 1 : []
{- However, it is much more convenient to write them using the compact
list notation: a sequence of values surrounded by square brackets,
separated by commas. The three examples we gave above are written
like so in this notation: -}
ex1'' :: [Int]
ex1'' = [2,7,1]
-- ex2'' :: [??]
-- ex2'' = [[], 1]
ex3'' :: [[a]]
ex3'' = [[], []]
{- PART 2 : TRANSFORMING DATA BY PATTERN MATCHING
If Haskell only had data, then it would not be a very interesting
programming language (though it could still be a useful data
language like JSON or XML).
To transform data in Haskell, we define functions by /pattern
matching/. This means that every function is a list of patterns of
data that it can match with, and for each pattern a description of
how that data is transformed.
Here is an example, which totals up all the elements in a list of
'Int's: -}
total :: List Int -> Int
total Nil = 0
total (Cons x xs) = x + total xs
{- As above, definitions consist of multiple lines. Here we have three
lines: one type definition line, and two patterns. The first line
describes the type of the thing we are defining. In this case, we
are defining 'total' which has type 'List Int -> Int'. We read this
as "functions that take Lists of Ints and return Ints". The
analogous Java type would be:
int total(List<Integer> input)
Note that Haskell types go left to right!
The second and third lines describe the two patterns of data that
'total' matches on, and what it does in those two cases.
'total Nil = 0' says "when total is applied to 'Nil', the answer is
'0'". Put another way, the total of the empty list is '0'.
'total (Cons x xs) = x + total xs' says "when total is applied to
'Cons x xs', the answer is 'x' added to whatever the total of 'xs'
is".
NOTE: We have used a naming convention common in Haskell
programming. The 'head' element of the list is called 'x'
(because it is some unknown), and the rest of the list is called
'xs' -- the "plural" of 'x'. In general, the '-s' suffix is used
for lists.
NOTE: Haskell programmers are sometimes criticised for their use
of short names like 'x' and 'xs', rather than longer names like
'theNumber' and 'restOfTheList'. Our feeling is that, while
Haskell programmers do sometimes go overboard with short names,
short names are very useful for maintaining a clear view of the
*shape* of the code. When defining functions by pattern
matching, it is often the shapes that are important, not the
specifics.
Running 'total' on some lists in GHCi should convince us that it is
actually computing the totals of lists of Ints:
*Lec01> total Nil
0
*Lec01> total (Cons 1 Nil)
1
*Lec01> total (Cons 1 (Cons 3 Nil))
4
*Lec01> total (Cons 1 (Cons 3 (Cons 5 Nil)))
9
We can also see how total works by stepping through the pattern
matching process by hand. Let's take the third example:
total (Cons 1 (Cons 3 Nil))
= by the second rule for total
1 + total (Cons 3 Nil)
= by the second rule for total
1 + (3 + total Nil)
= by the first rule for total
1 + (3 + 0)
= by (built in) arithmetic
1 + 3
= by (built in) arithmetic
4
As you'll've seen in the "Evaluation Game" exercise, quite complex
behaviour can be built up by pattern matching and reduction.
Note that 'total' can only be applied to data that is of type 'List
Int'. If we try to apply 'total' to a list of booleans ('True's and
'False's), then GHCi will complain:
*Lec01> total (Cons True Nil)
<interactive>:14:13:
Couldn't match expected type ‘Int’ with actual type ‘Bool’
In the first argument of ‘Cons’, namely ‘True’
In the first argument of ‘total’, namely ‘(Cons True Nil)’
Importantly, GHCi complained *before* trying to execute the
program. In Haskell, all type checking occurs before execution
time. For this reason, Haskell is known as a "statically typed"
language. This puts it in the same category as Java, though, as
you'll see in this course, Haskell's type system is more expressive
than Java's. An alternative (called "dynamic typing") is
implemented in languages like Javascript, Python, Scheme, and other
languages in the Lisp family.
Let's see what would happen in a version of Haskell without a type
checker:
total (Cons True Nil)
= by the second rule for total
True + total Nil
= by the first rule for total
True + 0
= cannot add booleans to Ints!
<< ERROR >>
In Haskell, we try to avoid errors occurring at runtime like this
by using types. Types are also useful as machine checked
documentation for programs that describe the sorts of data that are
expected as inputs and outputs for each part of the program. As we
will see in this course, and much more so in the CS410 course in
the fourth year, we can also use types to guide the process of
writing programs. Our philosophy is that types are a design
language that aids and guides us in writing programs. -}
{- 'total' is not the only function we can define on 'List's. Another
useful function is 'append', which concatenates two lists. We again
define this by pattern matching: -}
append :: List a -> List a -> List a
append Nil ys = ys
append (Cons x xs) ys = Cons x (append xs ys)
{- The type of 'append' states that it takes a 'List' of 'a's, another
'List' of 'a's, and returns a 'List' of 'a's. Note that 'append' is
polymorphic (or "generic") in the actual type of elements of the
lists. We do not need to write a separate append function for lists
of Ints and lists of Strings.
Line two states that, when the first argument is 'Nil', the result
is the second argument 'ys' (following the naming convention we
described above). This makes sense: appending a list on to the
empty list should just return the list.
Line three states that, when the second argument is 'Cons x xs',
the result is 'Cons x (append xs ys)'. That is -- we append xs to
ys, and put the 'x' on the front. It might not be easy to see that
this works straight away. Here is the example we used in the
lecture:
append (Cons "Unicorn" (Cons "Rainbow" Nil)) (Cons "Pegasus" Nil)
= { by the first rule }
Cons "Unicorn" (append (Cons "Rainbow" Nil) (Cons "Pegasus" Nil))
= { by the first rule }
Cons "Unicorn" (Cons "Rainbow" (append Nil (Cons "Pegasus" Nil)))
= { by the second rule }
Cons "Unicorn" (Cons "Rainbow" (Cons "Pegasus" Nil))
So, 'append' has successfully concatenated the two lists. You will
have seen many more examples of 'append' in action in the
Evaluation Game.
We can also get GHCi to check our work:
*Lec01> append (Cons "Unicorn" (Cons "Rainbow" Nil)) (Cons "Pegasus" Nil)
Cons "Unicorn" (Cons "Rainbow" (Cons "Pegasus" Nil))
Finally, we mention that, just as Haskell has a type of lists
pre-defined, it also has functions for adding up lists and
appending lists pre-defined. The function to add up a list is
called 'sum' (indeed 'sum' is more general and can be used to add
up any container containing numbers), and the function to append
two lists is called '++' and is written in infix notation:
*Lec01> ["Unicorn", "Rainbow"] ++ ["Pegasus"]
["Unicorn","Rainbow","Pegasus"]
This concludes our first introduction to Haskell's data, functions,
and types. In the next lecture, we will discuss some of the
standard types that are pre-defined in Haskell, and the data that
inhabits those types. -}