-
Notifications
You must be signed in to change notification settings - Fork 0
/
lecture1.txt
440 lines (287 loc) · 11.1 KB
/
lecture1.txt
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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
Korekce 1. přednášky
====================
I. Obecné
---------
Ačkoliv je do jisté míry pravda, že Haskell je ovlivněn prací Haskell B.
Curryho, tvrdit, že je Haskell dílem jeho žáků je přehnané. Pro zájemce
uvádím odkaz na
A History of Haskell: Being Lazy With Class
Paul Hudak, John Hughes, Simon Peyton Jones, Philip Wadler; 2007
http://research.microsoft.com/en-us/um/people/simonpj/papers/history-of-haskell/history.pdf
Funkce v Haskellu skutečně berou pouze jeden argument, ale tohle je v rozporu
s pozdějším Krylovo tvrzením, že konstanty jsou vlastně nulární funkce. Haskell
na úrovni typů velice ostře rozlišuje funkce od konstant.
Tedy například:
f :: Int -> Char -- funkce
x :: Int -- konstanta
l :: [Int -> Char] -- list funkcí, stále konstanta
Pokud si přepíšete typovou signaturu jako strom, tak funkce jsou právě ty
hodnoty, které mají šipku (->) v kořeni stromu. Příklad:
f :: Int -> Char -> (Bool, String)
(->)
/ \
/ \
Int (->)
/ \
/ \
Char (,)
/ \
/ \
Bool String
l :: [Int -> Int]
[ ]
|
|
(->)
/ \
/ \
Int Int
Zde je také nutné podotknout, že "funkční šipka" je operátor, který se
asociuje vpravo, proto si můžeme dovolit vynechat zbytečné závorky.
a -> b -> c == a -> (b -> c)
ale pozor!
a -> b -> c /= (a -> b) -> c
V tomto případě je a -> b -> c funkce, která bere jeden argument typu a
a vrací funkci typu b -> c, zatímco (a -> b) -> c je funkce, která bere
jako argument FUNKCI typu a -> b a vrací c.
Na toto téma mohu doporučit (relativně krátký) blog post Conala Elliota:
http://conal.net/blog/posts/everything-is-a-function-in-haskell
II. Syntaxe
-----------
II.I. Funkce
------------
Co Kryl na přednášce nezmínil a přitom je to velice podstatné (hlavně abyste
neskončili u metody pokus/omyl a také abyste psali pěkný kód) je syntaxe
pro aplikaci funkcí.
V Haskellu totiž žádná syntaxe pro aplikaci funkcí není, aplikace je
jednoduchá juxtapozice výrazů (tj. dáte dvě věci za sebe), závorky slouží
pouze ke seskupování výrazů. Několik příkladů:
add :: Int -> Int -> Int
add = (+)
-- 1 + 2
add 1 2
-- (1 + 2) + 3
add (add 1 2) 3
-- 1 + (2 + 3)
add 1 (add 2 3)
pozor:
add 1 add 2 3
se pokouší volat funkci 'add' s 4 argumenty, speciálně 1, add, 2 a 3.
f :: c -> d
g :: a -> b -> c
-- f(g(x, y))
f (g x y)
opět
f g x y
se pokouší volat funkci 'f' s 3 argumenty, g, x a y.
Prefixní aplikace funkcí má nejvyšší prioritu, tj.
f x + g y
je vždy parsováno jako
(f x) + (g y)
bez ohledu na prioritu operátoru +
Dále Kryl zmínil převádění prefixních funkcí na funkce infixní, k čemuž bych
jen dodal pár příkladů, kdy se tohle může hodit:
sum :: [Int] -> Int
sum list = foldr (+) 0 list
halfs :: [a] -> ([a], [a])
halfs list = splitAt (length list `div` 2) list
Speciálně se tedy dají dělat podobné srandy:
memberOf :: Int -> [Int] -> Bool
memberOf x xs = ... -- x se nalézá v seznamu xs
if 1 `memberOf` someList then ... else ...
II.II. Typové třídy
-------------------
Další poznámka se týká kontextů. Kryl ukazoval příklad:
f :: [Int] -> [Int]
f list = [2 * x | x <- list]
Tahle funkce ovšem funguje pro více věcí, než jen 'Int'.
f :: [a] -> [a]
je však moc obecné (násobení není definováno např. pro funkce).
Num a => f :: [a] -> [a]
je však špatně, všechny kontexty skutečně přijdou na začátek typové signatury
před =>, ale až po ::, správně je tedy:
f :: Num a => [a] -> [a]
Kontexty se dají libovolně kupit, např. tedy:
f :: (Ord a, Num a, Show b) => a -> [b]
tento kontext specifikuje podmínku, že typ a musí mít uspořádání (Ord) a musí
podporovat numerické operace (Num) a že typ b musí být "showable" (Show).
Pokud chcete vědět, co jednotlivé typové třídy obsahují za funkce, můžete
v GHCi použít příkaz :i třída
ghci> :i Ord
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(>=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(<=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
-- Defined in `GHC.Classes'
...
II.III. Datové typy
-------------------
Dále tu máme definice nových typů a keyword "data". Uvažme opět binární
stromy:
data Tree a
= Nil
| Node (Tree a) a (Tree a)
"data" deklaruje nový datový typ.
"Tree" je typový konstruktor, za kterým následuje seznam parametrů. Můžete se
na to dívat jako na šablony v C++ nebo generics v Javě a C#. Příklad:
data Tree a = ...
přibližně odpovídá
template <typename A>
class Tree
{ ... };
Zde je krásně vidět rozdíl mezi typem a typovým konstruktorem, stejně jako
v C++ nemůžete napsat:
Tree t = ...;
tak v Haskellu nelze použít:
f :: Tree -> ...
Tree není sám o sobě datový typ, očekává ještě jeden parametr, tedy:
Tree<int> t = ...;
f :: Tree Int -> ...
Za rovnítkem následuje výčet možných hodnot, kterých může tento datový typ
nabývat. Jednotlivé položky jsou odděleny svislítkem. Typ 'Bool' je např.
definován takto:
data Bool
= False
| True
Každá alternativa definuje datový konstruktor a případné argumenty, např:
data IntPair
= Pair Int Int
data IntList
= ListNil
| Cons Int IntList
Nulární konstruktory jsou potom konstanty daného datového typu.
Je velice důležité si uvědomit, že konstruktory se dají použít jako obyčejné
funkce, tj. z předchozích deklarací dostáváme:
Nil :: Tree a
Node :: Tree a -> a -> Tree a -> Tree a
False :: Bool
True :: Bool
Pair :: Int -> Int -> IntPair
ListNil :: IntList
Cons :: Int -> IntList -> IntList
Můžeme tedy provést například toto:
-- add new element to every IntList in a list
f :: Int -> [IntList] -> [IntList]
f number list = map (Cons number) list
II.IV. Pattern matching
-----------------------
Nyní se dostáváme k nejdůležitější části a to je pattern matching. Pár
poznámek k vyhodnocování a odpověď na nezodpovězenou otázku z přednášky.
crazy :: [[a]] -> Int
crazy ([]:xs) = 1
crazy (x:[y]:xs) = 2
crazy ([]:[]:xs) = 3
crazy _ = 4
Vyhodnocování probíhá shora dolů, tj. použije se pravá strana prvního
patternu, který uspěje. Narozdíl od Prologu Haskell nezkouší jiné možnosti a
taky nepodporuje unifikaci v Prologovském smyslu. NEMŮŽETE tedy napsat toto:
same :: (a, a) -> Bool
same (x, x) = True
same _ = False
Proměnná se v patternu může vyskytnout právě jednou, tj. musíte napsat:
same :: Eq a => (a, a) -> Bool -- budeme potřebovat test na rovnost
same (a, b) = a == b
Když se nad tím zamyslíte, dává to smysl. Představte si, že funkci 'same'
předáme dvojici FUNKCÍ. Unifikace funkcí, speciálně tedy test na rovnost, je
algoritmicky nerozhodnutelný problém (viz Halting Problem).
Než se do Haskellu ponoříte trochu více, tak zatím můžete používat jednoduché
pravidlo k závorkování: závorkujte každý netriviální pattern. Polovina Krylových
příkladů vám nebude fungovat právě kvůli špatnému závorkování.
root :: Tree a -> a
root Node l x r = x
Pokud napíšete toto, snažíte se definovat funcki 4 proměnných. Kromě toho,
že 'Node' samotný není pattern, tak dostanete typovou chybu, protože typová
signatura funkce 'root' specifikuje pouze 1 argument. Správně je:
root (Node l x r) = x
Závorky se dají vynechat u triviálních patternů, tj. např.
map _ [] = []
map f (x:xs) = f x:map f xs
Definování funkcí výčtem "rovnic" je pouze syntaktická zkratka za "case",
předchozí definici tedy můžeme zapsat jako:
map f list = case list of
[] -> []
(x:xs) -> f x:map f xs
Tímto způsobem pak můžeme provádět pattern matching i uvnitř anonymních
funkcí:
\list -> case list of
[] -> ...
(x:xs) ->
V některých případech lze pattern napsat přímo do seznamu argumentů, ale
tohle se hodí pouze pokud neexistuje více alternativ, kterých daná hodnota může
nabývat (pak ošetříte pouze jeden případ a funkce "spadne" pro některé vstupy).
\(x, y) -> x + y
předchozí anonymní funkce má typ Num a => (a, a) -> a.
II.V. Operátory
---------------
Prioritu operátorů si můžete sami zjisit pomocí GHCi takto:
ghci> :i :
data [] a = ... | a : [a] -- Defined in `GHC.Types'
infixr 5 :
ghci> :i +
class Num a where
(+) :: a -> a -> a
...
-- Defined in `GHC.Num'
infixl 6 +
infixr označuje pravou, infixl levou asociativitu, číslo je pak priorita. Čím
vyšší číslo, tím těsněji se daný operátor váže. Příklad:
infixl 6 +
infixl 7 *
a + b * c
se naparsuje jako
a + (b * c)
infixl 5 # -- levá asociativita
a # b # c
se naparsuje jako
(a # b) # c
Ale naproti tomu:
infixr 5 # -- pravá asociativita
a # b # c
se naparsuje jako
a # (b # c)
II.VI. Intervaly
----------------
Správná syntaxe pro intervaly je:
[1 ..] -- od 1 dále
[1, 3 ..] -- od 1 dále s krokem 2
[1 .. 5] -- od 1 do 5
[1, 3 .. 5] -- od 1 do 5 s krokem 2
Všimněte si, že funkce:
fromTo x y = [x .. y]
má typ Enum a => a -> a -> [a], což naznačuje, že tato syntaxe funguje pro více
věcí než čísla. Zeptejme se, co umí typová třída Enum.
ghci> :i Enum
class Enum a where
succ :: a -> a
pred :: a -> a
toEnum :: Int -> a
fromEnum :: a -> Int
enumFrom :: a -> [a]
enumFromThen :: a -> a -> [a]
enumFromTo :: a -> a -> [a]
enumFromThenTo :: a -> a -> a -> [a]
-- Defined in `GHC.Enum'
instance Enum Ordering -- Defined in `GHC.Enum'
instance Enum Integer -- Defined in `GHC.Enum'
instance Enum Int -- Defined in `GHC.Enum'
instance Enum Char -- Defined in `GHC.Enum'
instance Enum Bool -- Defined in `GHC.Enum'
instance Enum () -- Defined in `GHC.Enum'
instance Enum Float -- Defined in `GHC.Float'
instance Enum Double -- Defined in `GHC.Float'
Je tu například instance pro typ 'Char'. Můžeme tedy napsat:
['a' .. 'z'] pro "abcdefghijklmnopqrstuvwxyz"
Poznámka:
[a .. ] se převádí na volání enumFrom
[a, b .. ] enumFromThen
[a .. c] enumFromTo
[a, b .. c] enumFromThenTo
III. Závěrem
------------
Celý tento dokument je sepsán velmi narychlo, pokud najdete chybu nebo si dáte
práci s pěknějším formátováním, můžete mě kontaktovat na adrese:
vituscze@gmail.com