-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlec12.hs
210 lines (162 loc) · 9.95 KB
/
lec12.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
import Control.Monad
-- Домашнее задание находится в конце файла.
-------------------------------------------------
-- Конспект лекции 12 от 03.05.2021
-------------------------------------------------
-- Содержание
-- 1. Объявление монады.
-- 2. Функция fail.
-- 3. Использование списочной монады для решения задач перебором.
-------------------------------------------------
-- 1. Объявление монады
-------------------------------------------------
-- Около 2015 г. порядок объявления конструктора типа монадой
-- значительно изменился. Ранее класс Monad определялся так.
-- class Monad m where
-- (>>=) :: m a -> (a -> m b) -> m b
-- (>>) :: m a -> m b -> m b
-- return :: a -> m a
-- fail :: String -> m a
-- -- Определение по умолчанию функций (>>) и fail
-- m >> k = m >>= \_ -> k
-- fail s = error s
-- Из определения следует, что класс Monad не являлся подклассом
-- другого класса (см. Prelude, где, например, класс Ord типов с
-- линейным порядком является подклассом Eq, а класс Fractional
-- является подклассом Num). Поскольку функции (>>) и fail имели
-- определение по умолчанию, при объявлении конструктора типа монадой
-- нужно было определить только (>>=) и return. Функция fail будет
-- обсуждаться позже, а определение (>>) обычно не меняется.
-- Понятие монады идет из теории категорий, и с математической точки
-- зрения монада является так называемым аппликативным
-- функтором. Поэтому начиная с Haskell 7.10 было решено класс Monad
-- был сделан подклассом Applicative, который в свою очередь является
-- подклассом Functor. Теперь нужно объявить, что некоторый конструктор
-- типа m принадлежит, во-первых, классу Functor, во-вторых, классу
-- Applicative, и только потом классу Monad.
-- Итак, вместо
-- instance Monad Foo where
-- return x = retDef
-- m >>= k = thenDef
--
-- нужно писать следующее (пока это не требуется понимать).
--
-- instance Functor Foo where
-- fmap = liftM
--
-- instance Applicative Foo where
-- pure = retDef
-- (<*>) = ap
--
-- instance Monad Foo where
-- m >>= k = thenDef
-- В определениях выше меняются только retDef и thenDef; всё остальное
-- печатается буквально.
-- Важно: для определений выше нужно импортировать модуль Control.Monad.
-- Более подробно о переходе см. https://ghc.haskell.org/trac/ghc/wiki/Migration/7.10
-- Например, вот как можно объявить Maybe монадой (объявим новый тип
-- со штрихами, чтобы не путаться с определениями, данными в Prelude).
data Maybe' a = Nothing' | Just' a deriving Show
-- До Haskell 7.10
-- instance Monad Maybe' where
-- (Just' x) >>= k = k x
-- Nothing' >>= _ = Nothing'
--
-- return = Just'
-- После Haskell 7.10
instance Functor Maybe' where
fmap = liftM
instance Applicative Maybe' where
pure = Just' -- бывшее определение return
(<*>) = ap
instance Monad Maybe' where
(Just' x) >>= k = k x
Nothing' >>= _ = Nothing'
-------------------------------------------------
-- 2. Функция fail
-------------------------------------------------
-- Кроме return и >>=, для всех конструкторов типов m, принадлежащих
-- классу Monad, должна быть определена функция fail :: String -> m a.
-- Класс Monad дает реализацию по умолчанию: fail s = error s, поэтому
-- при объявлении новой монады определять fail не обязательно. Тем не
-- менее, в некоторых монадах ее определение отличается. Так, в Maybe
-- определено fail _ = Nothing, а в списочной монаде fail _ = [].
-- Функция fail используется при наличии образцов в do-нотации. В предыдущей
-- лекции говорилось, что
-- do p <- e
-- stmts
-- преобразуется в e >>= (\p -> do {stmts}). Здесь p может быть не только
-- переменной или _, но и произвольным образцом. Если чистое значение,
-- извлеченное из монадного значения e, не может быть сопоставлено с
-- образцом p, то вызывается fail с подходящим сообщением об ошибке.
-- Таким образом, do-выражение выше на самом деле преобразуется в
-- следующее выражение.
-- let ok p = do {stmts}
-- ok _ = fail "..."
-- in e >>= ok
-- Первые две строчки определяют функцию ok. В простейшем случае,
-- когда p есть переменная, она эквивалентна \p -> do {stmts}, однако
-- в случае невозможности сопоставления аргумента с образцом p она
-- вызывает fail с сообщением, генерируемым интерпретатором. Затем эта
-- функция используется в качестве второго аргумента >>=.
-- Между do-нотацией и генераторами списков (см. лекцию 3) есть тесная связь.
-- Например, следующие два значения равны.
listComprehension n = [(x, y) | x <- [1..n], y <- [1..n-1], gcd x y == 1]
listMonad n = do
x <- [1..n]
y <- [1..n-1]
True <- return (gcd x y == 1)
return (x, y)
-- На самом деле, в прежних версиях Haskell синтаксис, аналогичный
-- генераторам списков можно было использовать для любой монады,
-- а не только для списочной.
-------------------------------------------------
-- 3. Использование списочной монады для решения задач перебором
-------------------------------------------------
-- В списочной монаде fail можно использовать, чтобы показать, что одно
-- из недетерминированных вычислений закончилось неудачей и его результат
-- не следует включать в общий список результатов. Предположим, мы хотим
-- получить все последовательности 0 и 1 длины n, сумма которых четна.
-- Это можно сделать следующим образом.
allEvenSeq :: Int -> [[Int]]
allEvenSeq n = go n [] where
go 0 s
| even (sum s) = return s
| otherwise = fail ""
go n s = do
x <- [0,1]
go (n-1) (x : s)
-- Рассмотрим еще одну задачу. Пусть имеется вычислительное устройство
-- с одним регистором, в котором хранится целое число. Устройств имеет
-- две команды: прибавить 2 к содержимому регистра и умножить
-- содержимое регистра на 3, после чего заменить содержимое регистра
-- на новое. Это устройство можно представить в Haskell следующим
-- образом.
data Command = A2 | M3 deriving (Eq, Show, Enum, Bounded)
-- Семантика команд
sem :: Command -> Int -> Int
sem A2 = (+ 2)
sem M3 = (* 3)
-- commands = [A2, M3], то есть список всех команд. При добавлении
-- новых команд в типе Command выше следующее определение изменять
-- не нужно.
commands :: [Command]
commands = [minBound .. maxBound]
-- programs m n [] возвращает список последовательностей команд,
-- которые преобразуют содержимое регистра из m в n.
programs :: Int -> Int -> [Command] -> [[Command]]
programs m n coms
| m == n = return (reverse coms)
| m > n = fail ""
| otherwise = do
c <- commands
programs (sem c m) n (c : coms)
-- Например, преобразовать 1 в 39 можно 20 способами, так как
-- length (programs 1 39 []) возвращает 20.
-------------------------------------------------
-- Домашнее задание
-------------------------------------------------
-- На доске написана пара чисел. За один шаг разрешается стереть пару
-- (x, y) и на ее место написать либо (2*x, y+1), либо (x+1, 2*y).
-- Найдите наименьшее число x, такое что пару (x, x) можно получить
-- из пары (28, 64).