-
Notifications
You must be signed in to change notification settings - Fork 9
/
lecture4.tex
501 lines (460 loc) · 26.7 KB
/
lecture4.tex
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
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
\documentclass[10pt]{beamer}
\input{lecture_preamble.tex}
\title{Лекция 4: классы типов}
\begin{document}
\begin{frame}[plain]
\maketitle
\end{frame}
\begin{frame}[fragile]
\frametitle{Классы типов}
\begin{itemize}
\item Полиморфные функции, которые мы видели в прошлый раз, имеют одно определение для любых параметров типов.
\item Часто нужно определить функции по-разному для разных типов.
\item Для этого используются классы типов.
\begin{haskell}
class Eq a where
(==), (/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
\end{haskell}
\item \haskinline|Eq| "--- название класса.
\item \haskinline|a| "--- тип, относящийся к этому классу.
\item \haskinline|(==)| и \haskinline|(/=)| "--- функции-члены класса, с определениями по умолчанию.
\item Их нужно определить для типа \haskinline|a| \pause(достаточно одну).
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Экземпляры классов}
\begin{itemize}
\item Чтобы объявить, что тип \haskinline|Bool| относится к классу \haskinline|Eq|:
\begin{haskell}
instance Eq Bool where
-- (==) :: Bool -> Bool -> Bool
True == True = True
False == False = True !\pause!
_ == _ = False
\end{haskell}
\item При этом \haskinline|a| в сигнатуре членов заменяется на тип, для которого определяется экземпляр.
\item Ещё пример:\pause
\begin{haskell}
instance Eq IpAddress where
-- (==) :: IpAddress -> IpAddress -> Bool
Ip4Address x == Ip4Address y = x == y
Ip6Address x == Ip6Address y = x == y
_ == _ = False
\end{haskell}
\only<3>{\item Рекурсивно ли это определение?}
\item<4-> Это не рекурсия: \haskinline|==| в правой части относится к другому типу (\haskinline|String|).
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Ограниченный полиморфизм}
\begin{itemize}
\item Если спросить GHCi про тип \haskinline|==|, получим
\begin{haskell}
ghci> :t (==)
(==) :: Eq a => a -> a -> Bool
\end{haskell}
\item Читается \enquote{\haskinline|a -> a -> Bool| для любого \haskinline|a| из \haskinline|Eq|}.
\item Часть перед \haskinline|=>| называется контекстом.
\item Его элементы "--- ограничения. Когда их больше одного, они пишутся в скобках через запятую.\pause
\item Типы полиморфных функций, \emph{использующих} \haskinline|==| и \haskinline|/=|, пишутся аналогично. Определим:
\begin{haskell}
foo [] = True
foo [x] = True
foo (x : tail@(y : _)) = x == y && foo tail
\end{haskell}
Какой у неё тип (и смысл)? \pause
\item \haskinline|foo :: Eq a => [a] -> Bool| \pause
\item Функция проверяет, равны ли все элементы списка.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Экземпляры для полиморфных типов}
\begin{itemize}
\item Попробуем определить \haskinline|Eq| для \haskinline|Maybe a|:\pause
\begin{haskell}
instance Eq (Maybe a) where
-- (==) :: Maybe a -> Maybe a -> Bool
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
\end{haskell}
\only<2>{\item Верное ли это определение?} \pause
\item Здесь \haskinline|a| не обязательно \haskinline|Eq|, и поэтому \haskinline|x == y| \\не скомпилируется.
\item А можно ли так?
\begin{haskell}
instance Eq (Maybe a) where
(==) :: Eq a => Maybe a -> Maybe a -> Bool
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
\end{haskell}
\pause
\item Нет: эта сигнатура для \haskinline|(==)| более ограничена, чем заданная классом.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Экземпляры для полиморфных типов \\с ограничениями}
\begin{itemize}
\item Правильно так:
\begin{haskell}
instance Eq a => Eq (Maybe a) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
\end{haskell}
\item Читаем \enquote{\haskinline|Maybe a| относится к \haskinline|Eq| только тогда, когда \haskinline|a| относится к \haskinline|Eq|}.
\item Контексты экземпляров подчиняются тем же правилам, что контексты функций.
\item Ограничения могут упоминать другие классы, а не только тот, экземпляр которого определяется.
\item Не может быть много экземпляров одного класса для одного типа с разными условиями.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Аналоги в других языках}
\begin{itemize}
\item Как перегрузка функций (или операторов), но глубже встроенная в систему типов.
\item Перегруженные функции "--- просто разные функции, связанные только общим именем.
\item Здесь обязательно близкие типы и общий смысл.
\item И функции могут быть использованы другими полиморфными функциями. \pause
\item Не путайте классы типов и классы в ООП, общее только название!
\item В C++ есть близкое понятие концепций. \pause
\item До C++20 было только для документации, а теперь их можно объявлять и использовать в коде. \pause
\item Часто сравнивают с интерфейсами в Java/C\#, но это подходит хуже: \pause в классах типов есть \enquote{статические} и бинарные методы, совсем другой смысл функции, возвращающей такой тип.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Другие важные классы}
\begin{itemize}
\item
Линейно упорядоченные типы:
\begin{haskell}
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<), (<=), (>), (>=) :: a -> a -> Bool
max, min :: a -> a -> a
\end{haskell}
\begin{itemize}
\item \haskinline|Eq a| "--- надкласс \haskinline|Ord|, то есть любой экземпляр \haskinline|Ord| должен быть и экземпляром \haskinline|Eq|. \pause
\item Логически стрелка скорее в другом направлении:\\ \haskinline|a| относится к \haskinline|Ord| $\implies$ \haskinline|a| относится к \haskinline|Eq|.
\end{itemize}
\pause
\item
Типы, ограниченные сверху и снизу:
\begin{haskell}
class Bounded a where
minBound, maxBound :: a
\end{haskell}
\begin{itemize}
\item Это не подкласс \haskinline|Ord| потому, что порядок, для которого эти значения "--- границы, может быть частичным.
\end{itemize}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Преобразования в строки и обратно}
\begin{itemize}
\item
Типы, значения которых можно превратить в строки:
\begin{haskell}
class Show a where
show :: a -> String
...
\end{haskell}
\pause
\item Или прочитать из строк:
\begin{haskell}
class Read a where ...
read :: Read a => String -> a
\end{haskell}
\pause
\item Для нас то, что за многоточиями, неважно, но подробности описаны \href{http://hackage.haskell.org/package/base-4.10.1.0/docs/Prelude.html#g:22}{в документации}.\pause
\item \haskinline|read| должна быть обратной к \haskinline|show|.
\item Заметьте полиморфизм \haskinline|read| по возвращаемому типу:
\begin{haskell}
ghci> read "2" :: Integer
2
ghci> read "2" :: Double
2.0
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Опасность \haskinline|read|}
\begin{itemize}
\item Что случится, если передать \haskinline|read| неподходящий аргумент?
\begin{haskell}
ghci> read "1" :: [Bool] !\pause!
\end{haskell}
\begin{ghci}
*** Exception: Prelude.read: no parse
\end{ghci}
\item Если вы не знаете \emph{точно}, что текст содержит значение, используйте безопасную
\begin{haskell}
readMaybe :: Read a => String -> Maybe a
\end{haskell}
из модуля \haskinline|Text.Read|.
\pause
\item[]
\item Это не единственная функция в Prelude с такой проблемой.
\item Пакет \hackage{safe} содержит безопасные варианты для всех таких функций (посмотрите его документацию).
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Класс перечислимых типов}
\begin{itemize}
\item
\haskinline|Enum| содержит типы, значения которых образуют последовательность.
\begin{haskell}
class Enum a where
succ, pred :: a -> a
toEnum :: Int -> a
fromEnum :: a -> Int
enumFrom :: a -> [a] -- и ещё 3 вариации
\end{haskell}
\pause
\item Для него есть специальный синтаксис:
\begin{itemize}
\item \haskinline|[n..]| означает \haskinline|enumFrom n|;
\item \haskinline|[n..m]| "--- \haskinline|enumFromTo n m|;
\item \haskinline|[n,m..]| "--- \haskinline|enumFromThen n m|;
\item \haskinline|[n,m..l]| "--- \haskinline|enumFromThenTo n m l|.
\end{itemize}
\pause
\item Для \haskinline|Float| и \haskinline|Double| есть экземпляры \haskinline|Enum|, так что можно писать \haskinline|[1.0..]|\only<4->{, но они довольно странные}:
\begin{haskell}
ghci> [1.2..2.0] !\pause!
[1.2,2.2]
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Иерархия числовых классов}
\begin{itemize}
\item Вернёмся к \enquote{числовым типам} из первой лекции.
\item Это тоже классы типов, образующие довольно сложную иерархию.\pause
\begin{haskell}
class Num a where
(+), (-), (*), negate, abs, signum, fromInteger
class (Num a, Ord a) => Real a where
toRational
class (Real a, Enum a) => Integral a where
quot, rem, div, mod, quotRem, divMod
toInteger
class Num a => Fractional a where
(/), recip, fromRational
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Иерархия числовых классов: дробные типы}
\begin{itemize}
\begin{haskell}
class Fractional a => Floating a where
exp, log, sqrt, (**), logBase,
pi, sin, cos, ...
class (Real a, Fractional a) => RealFrac a where
properFraction :: Integral b => a -> (b, a)
truncate, round, ceiling, floor ::
Integral b => a -> b
class (RealFrac a, Floating a) => RealFloat a where
isNaN, isInfinite :: a -> Bool
...
fromIntegral :: (Integral a, Num b) => a -> b
realToFrac :: (Real a, Fractional b) => a -> b
\end{haskell}
\item \href{http://hackage.haskell.org/package/base-4.16.4.0/docs/Prelude.html#g:7}{Полная документация.}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Числовые литералы}
\begin{itemize}
\item Литерал \haskinline|0| означает \haskinline|fromInteger (0 :: Integer)| и соответственно получает тип \pause\haskinline|Num a => a|.
\item Аналогично для дробных: \haskinline|1.1| представляется как рациональное и превращается в \haskinline|fromRational ((11 % 10) :: Rational)| типа \haskinline|Fractional a => a|.
\item Здесь:
\begin{haskell}
module Data.Ratio where
data Ratio a -- экспортировано без конструктора
type Rational = Ratio Integer
(%) :: Integral a => a -> a -> Ratio a
\end{haskell}
\pause
\item Использование рациональных чисел для дробных литералов позволяет избежать ошибок округления.\pause
\item Конечно, при преобразовании в \haskinline|Float| или \haskinline|Double| они вернутся.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Алгебраические классы}
\begin{itemize}
\item Для разных структур из общей алгебры или линейной алгебры можно определить соответствующие им классы. В стандартной библиотеке есть
\item моноиды в \haskinline|Prelude|:
\begin{haskell}
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
x <> y = mappend x y -- синоним для mappend
\end{haskell}
\item и полугруппы в \href{https://hackage.haskell.org/package/base/docs/Data-Semigroup.html}{\haskinline|Data.Semigroup|}. \pause
\item Ещё несколько из теории категорий, но это предмет будущих лекций.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Определение функций внутри или вне класса}
\begin{itemize}
\item При определении функций над классом часто приходится выбирать из двух вариантов:
\begin{itemize}
\item член класса с определением по умолчанию;
\item функция, определённая вне класса.
\end{itemize}
\item Например, \haskinline|(/=)| можно было бы определить вне класса:
\begin{haskell}
(/=) :: Eq a => a -> a -> Bool
x /= y = not (x == y)
\end{haskell}
\pause
\item Такую функцию нельзя реализовать по-разному для разных экземпляров.\pause
\item Если для каких-то типов есть реализация лучше той, что по умолчанию, лучше сделать функцию членом класса.
\pause
\begin{itemize}
\item Очевидная проблема: вы можете не знать (или не подумать) о таких типах.
\end{itemize}
\pause
\item Или если реализация по умолчанию не всегда верна \pause(но тогда почему это реализация по умолчанию?).
\end{itemize}
\end{frame}
% TODO Похоже, неверно: compare оптимально? Поискать пример лучше
%\begin{frame}[fragile]
%\frametitle{Определение функций внутри или вне класса:\\случай \haskinline|Ord|}
%\begin{itemize}
% \item Ещё пример: в \haskinline|Ord| можно было бы оставить только \haskinline|(<=)| членом класса.
% \item Или только \haskinline|compare|.
% \item Но определение \haskinline|(<)| и других функций через них не всегда оптимально.
% \item (Подумайте, почему!)
%\end{itemize}
%\end{frame}
\begin{frame}[fragile]
\frametitle{Законы классов типов}
\begin{itemize}
\item Рассмотрим такой экземпляр типа:
\begin{haskell}
data Weird = Weird Int
instance Eq Weird where
Weird x == Weird y = x /= y
\end{haskell}
\item Даже не зная смысла \haskinline|Weird| мы видим, что что-то здесь не так. \only<1>{Что?}\pause
\item Это определение нерефлексивно, то есть \haskinline|Weird 1 /= Weird 1|.
\item У всех классов типов (почти) есть законы.
\item То есть наличия функций типов, указанных в определении, недостаточно;
\item для корректных экземпляров должны выполняться определённые законы.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Законы классов типов}
\begin{itemize}
\item Например, для \haskinline|Eq| законы такие:\pause
\begin{haskell}
!$\forall$! x :: a. x == x
!$\forall$! x, y :: a. x == y !$\implies$! y == x
!$\forall$! x, y, z :: a. x == y !$\wedge$! y == z !$\implies$! x == z !\pause!
!$\forall$! x, y :: a; Eq b; f :: a -> b. x == y !$\implies$\pause!
f x == f y
\end{haskell}
\pause
\item Законы могут говорить и о связи между классами. Например, если тип одновременно \haskinline|Ord| и \haskinline|Bounded|, то естественно требовать
\begin{haskell}
!$\forall$! x :: a. x <= maxBound
!$\forall$! x :: a. x >= minBound
\end{haskell}
\pause
\item Компилятор не может проверить законы.
\item Поэтому ответственность за их соблюдение лежит на программисте.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Автоматический вывод экземпляров}
\begin{itemize}
\item Почти все определения \haskinline|(==)| устроены одинаково:\pause
\begin{itemize}
\item Типы всех полей всех конструкторов должны быть \haskinline|Eq|;
\item Значения с одинаковым конструктором равны, если равны все их поля (или полей нет);
\item Значения с разными конструкторами не равны;
\end{itemize}
\item Почти, но не все: \pause для рациональных чисел не так.\pause
\item Чтобы не писать такие определения каждый раз, после \haskinline|data| или \haskinline|newtype| ставят \haskinline|deriving (Eq)|.
\item \haskinline|deriving| также работает для \haskinline|Ord|, \haskinline|Show|, \haskinline|Read|, \haskinline|Enum| и \haskinline|Bounded|.
\item Порядок на значениях для \haskinline|Ord|, \haskinline|Enum| и \haskinline|Bounded| задаётся порядком конструкторов в определении.\pause
\item Разные расширения GHC позволяют \haskinline|deriving| для других классов.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Классы типов и \haskinline|newtype|}
\begin{itemize}
\item Для каждой пары класса и типа можно определить только один экземпляр.
\item А что делать, если осмысленных экземпляров больше одного?
\item Например, числа "--- моноиды по операциям \haskinline|+| и \haskinline|*|.\pause
\item В таком случае мы можем завернуть существующий тип в \haskinline|newtype| и определить для нового:
\begin{haskellsmall}
newtype Sum a = Sum { getSum :: a }
deriving (Eq, Ord, Read, Show, Bounded, Num)
instance Num a => Monoid (Sum a) where
mempty = Sum 0
mappend (Sum x) (Sum y) = Sum (x + y) !\pause!
newtype Product a = Product { getProduct :: a }
deriving (Eq, Ord, Read, Show, Bounded, Num)
instance Num a => Monoid (Product a) where ...
\end{haskellsmall}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Классы типов и \haskinline|newtype|}
\begin{itemize}
\item Если заметили, \haskinline|Num| не было в списке классов, для которых есть автоматический вывод экземпляров.
\item Для \haskinline|newtype| можно вывести любой класс с помощью расширения \haskinline|GeneralizedNewtypeDeriving|.\pause
\item Выведенное определение каждой функции вызывает ту же функцию завёрнутого типа с вставкой и разбором конструкторов где нужно:
\begin{haskell}
instance Num a => Num (Sum a) where
Sum x + Sum y = Sum (x + y)
Sum x * Sum y = Sum (x * y)
negate (Sum x) = Sum (negate x)
...
\end{haskell}
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Классы типов и модули}
\begin{itemize}
\item \haskinline|Класс(..)| в списке импорта/экспорта означает класс со всеми методами, как для типа.
\item Поскольку экземпляры не имеют имён, они экспортируются из модуля всегда.
\item И импортируются всегда, когда модуль упоминается в списке импорта.\pause
\item[]
\item Из-за этого существование экземпляров для пары класс-тип не в одном модуле "--- проблема.
\item Поэтому экземпляры обычно определяются либо в том модуле, где определён тип, либо в том, где определён класс.
\item Остальные называются сиротами и по возможности их создания следует избегать.
\item Это делается с помощью того же трюка с \haskinline|newtype|.
\end{itemize}
\end{frame}
\begin{frame}[fragile]
\frametitle{Ограничения классов типов в стандартном Haskell и расширения GHC}
\begin{itemize}
\item В стандартном Haskell есть несколько ограничений на объявления классов типов и экземпляров.
\item А в GHC расширений, которые их снимают.
\item Все перечислять не будем, они есть в \href{http://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#class-declarations}{User's Guide, раздел 10.8}.
\item Сейчас достаточно знать, что если что-то не компилируется из-за отсутствия \haskinline|-XFlexibleInstances|, \haskinline|-XFlexibleContexts| или \haskinline|-XInstanceSigs|, их спокойно можно включить.
\item \haskinline|-XUndecidableInstances|, \haskinline|-XOverlappingInstances|, \haskinline|-XIncoherentInstances| опаснее, но в этом курсе в любом случае не понадобятся.
\end{itemize}
\end{frame}
% TODO восстановить слайды
%\begin{frame}[fragile]
%\frametitle{Классы с несколькими параметрами}
%\begin{itemize}
% \item TODO
%\end{itemize}
%\end{frame}
%
%\begin{frame}[fragile]
%\frametitle{Типы как члены классов}
%\begin{itemize}
% \item TODO
%\end{itemize}
%\end{frame}
\end{document}