-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlec14.hs
348 lines (269 loc) · 16.6 KB
/
lec14.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
{-# LANGUAGE FlexibleContexts #-}
import Prelude hiding (gcd)
import Data.Semigroup
import Control.Monad.Writer
-- Одним из достоинств использования монад в функциональном
-- программировании является модульность. Оно заключается в том, что
-- монаду можно менять при минимальном изменении кода основной
-- программы. Понятно, что два рассмотренных в лекции 13
-- варианта функций, вычисляющих числа Фибоначчи, можно написать и без
-- использования монад. Но тогда детали вычисления числа сложений,
-- коэффициентов линейной комбинации и т.п. будут перемешаны с
-- вычислением самих элементов последовательности, в то время как с
-- использованием монад различия локализованы в определении монад, а
-- функции выглядят почти одинаково.
-- Монады могут использоваться для реализации вычислений с состоянием
-- (изменяемые переменные), входом/выходом, сообщениями об ошибках,
-- недетерминизмом и т.п. При этом, как и в рассмотренных примерах,
-- добавление или удаление этих возможностей минимально отражается на
-- основной программе.
-- Не всякий конструктор типа с операциями return и >>= нужной
-- сигнатуры является монадой. От этих операций требуется, чтобы они
-- удовлетворяли трем законам. Это аналогично определению класса
-- Monoid в лекции 8. В Haskell можно только потребовать, чтобы в
-- моноиде были бинарная операция и единица, но нельзя гарантировать,
-- что эта операция ассоциативна, а единица действительно служит
-- единицей для этой операции.
-- Напомним определения типов Additions и LC из лекции 13.
-- newtype Additions a = A (a, Int) deriving Show
-- newtype LC a = LC (a, Int, Int) deriving Show
-- Как можно обобщить эти определения? Модуль Control.Monad.Writer
-- содержит определение, эквивалентное следующему.
-- newtype Writer w a = Writer { runWriter :: (a, w) } deriving Show
-- Напомним (см. лекцию 7), что это определение эквивалентно
-- следующим.
-- newtype Writer w a = Writer (a, w) deriving Show
-- runWriter :: Writer w a -> (a, w)
-- runWriter (Writer p) = p
-- Объявление newtype аналогично data с одним конструктором, который в
-- свою очередь имеет один аргумент. Таким образом, Writer w a — это
-- запись с единственные полем по имени runWriter. Одновременно
-- runWriter — это функция-проекция, которая по записи возвращает
-- значение поля.
-- Таким образом, конструктор значений Writer :: (a, w) -> Writer w a
-- и функция-проекция runWriter :: Writer w a -> (a, w) осуществляют
-- взаимно-однозначное соответствие между типами Writer w a и (a, w).
-- При чтении их следует пропускать. Например, результат оборачивания
-- пары (1, "abc") в тип Writer String Int записывается следующим
-- образом:
-- > Writer (1, "abc")
-- Writer {runWriter = (1,"abc")}
-- но понимать его нужно как (1, "abc"). Как сказано в лекции 7,
-- конструктор Writer и проекция runWriter имеют значение только при
-- проверке типов и прозрачны во время исполнения.
-- В определении Writer тип a — это тип чистых значений, а w — тип
-- так называемых сообщений, то есть дополнительной информации.
-- Сообщения образуют контекст, который вместе с чистыми значениями
-- составляет монадное значение. В Additions тип w есть Int, а в LC
-- тип w есть (Int, Int).
-- Обратите внимание, что конструктор типов Writer имеет два
-- типа-аргумента. Таким образом, частично примененный конструктор
-- Writer w для некоторого типа w можно рассматривать как функцию,
-- которая принимает еще один тип a и возвращает тип Writer w a.
-- Конструктор Writer w удобно объявить монадой. Эта ситуация похожа
-- на объявление в лекции 10 монадой конструктора типов Either,
-- примененного к одному из двух аргументов.
-- Роль функция >>= состоит в том, чтобы накапливать сообщения (вторые
-- элементы пар (a, w)). Как это происходит? В Addition накопление
-- представляет собой простое сложение чисел, а в LC — почленное
-- сложение упорядоченных пар чисел. Эти типы имеют нейтральные
-- элементы по сложению, которые комбинируются с чистыми значениями
-- функцией return. В Additions это 0, в LC — (0, 0). Обобщением типа
-- сообщений w может служить моноид. Напомним, что операция моноида
-- обозначается через (<>), а единица операции — через mempty.
-- В лекции 8 описывалось, как тип Integer может рассматриваться
-- моноидом по сложению и умножению. Прямое произведение моноидов
-- также является моноидом благодаря определению, похожему на
-- следующее.
-- instance (Monoid a, Monoid b) => Monoid (a, b) where
-- mempty = (mempty, mempty)
-- (a, b) <> (a', b') = (a<>a', b<>b')
-- Таким образом, операции на парах определены почленно.
-- Например:
-- > (1 :: Sum Int, 2 :: Product Int) <> (3, 4)
-- (Sum {getSum = 4},Product {getProduct = 8})
-- Здесь первая координата рассматривается по сложению, а вторая —
-- по умножению.
-- Итак, члены класса Monoid можно использовать в качестве типа
-- сообщений w в Writer. Напомним определения монад Additions и LC в
-- стиле до 2015 г., когда Monad не был подклассом Applicative.
-- instance Monad Additions where
-- return x = A (x, 0)
-- A (x, n) >>= f = let A (y, m) = f x in A (y, m+n)
-- instance Monad LC where
-- return x = LC (x, 0, 0)
-- LC (x, x0, x1) >>= f = let LC (y, y0, y1) = f x in LC (y, x0+y0, x1+y1)
-- Повторим определение конструктора типа Writer.
-- newtype Writer w a = Writer { runWriter :: (a, w) }
-- Вот как Writer можно объявить монадой, обобщая определения выше.
-- instance Monoid w => Monad (Writer w) where
-- return x = Writer (x, mempty)
-- mx >>= f = let (x, wx) = runWriter mx
-- (y, wy) = runWriter (f x) in
-- Writer (y, wx <> wy)
-- Итак, mx >>= f работает следующим образом. Сначала монадной
-- значение mx :: Writer w a разбивается на чистое значение x :: a и
-- сообщение wx :: w. Затем функция f :: a -> Writer w b применяется
-- к x, выдавая другое монадное значение. Оно разбивается на чистое
-- значение y :: b и сообщение wy :: w. Результатом является пара
-- (y, wx <> wy).
-- Как обычно, mx >> my означает mx >>= (\_ -> my), поэтому (опуская
-- Writer и runWriter), выражение (x, wx) >> (y, wy) редуцируется к
-- (y, mx <> my).
-- Модуль Control.Monad.Writer также экспортирует операции, которые
-- можно определить следующим образом.
-- tell :: w -> Writer w ()
-- tell w = Writer ((), w)
-- writer :: (a, w) -> Writer w a
-- writer (a, w) = tell w >> return a
-- что эквивалентно
-- writer (a, w) = Writer (a, w)
-- Таким образом, tell w возвращает тривиальное чистое значение (),
-- которое игнорируется, если tell используется в контексте
-- tell w >> ..., но сообщение w присоединяется (с помощью >>) к элементу
-- моноида, выполняющему роль аккумулятора сообщений. Функция
-- writer (a, w) одновременно возвращает чистое значение a и добавляет
-- сообщение w.
-- На самом деле определение конструктора типов Writer отличается от
-- приведенного выше. В частности, вместо конструктора значений
-- Writer :: (a, w) -> Writer w a нужно использовать функцию writer
-- того же типа.
-- Функции fibA и fibLC из лекции 13 можно написать следующим образом
-- с использованием Writer.
fibA :: Int -> Writer (Sum Int) Int
fibA 0 = return 0
fibA 1 = return 1
fibA n = do
p1 <- fibA (n-1)
p2 <- fibA (n-2)
tell 1
return (p1 + p2)
-- Здесь в качестве типа сообщений выступает Int, рассматриваемый как моноид
-- по сложению. Вызов tell 1 перед return добавляет 1 к счетчику сложений.
-- > fibA 5
-- WriterT (Identity (5, Sum {getSum = 7}))
-- Из-за того, что действительное определение Writer в
-- Control.Monad.Writer отличается, данное выражение возвращает не
-- ожидаемое значение
-- Writer {runWriter = (5, Sum {getSum = 7})}
-- Однако функция-проекция runWriter
-- возвращает нужную пару, состоящую из чистого значения и сообщения.
-- > runWriter $ fibA 5
-- (5, Sum {getSum = 7})
-- Последовательность, аналогичная числам Фибоначчи, с начальными
-- значениями a и b.
fibLC :: Int -> Int -> Int -> Writer (Sum Int, Sum Int) Int
fibLC a b n = go n where
go 0 = writer (a, (1, 0)) -- или tell (1, 0) >> return a
go 1 = writer (b, (0, 1)) -- или tell (0, 1) >> return b
go n = do
p1 <- go (n-1)
p2 <- go (n-2)
return (p1 + p2)
-- По техническим причинам для компиляции этой функции в начало файла
-- следует добавить инструкцию {-# LANGUAGE FlexibleContexts #-}
-- или дать команду :set -XFlexibleContexts в интерпретаторе.
-- > runWriter $ fibLC 0 1 5
-- (5, (Sum {getSum = 3}, Sum {getSum = 5}))
-- Монада Writer часто используется для журналирования (logging). В
-- этом случае в качестве моноида может использоваться String, то есть
-- [Char]. Списки образуют моноид согласно следующим определениям.
-- instance Semigroup [a] where
-- (<>) = (++)
-- instance Monoid [a] where
-- mempty = []
-- В книге Липовача "Изучай Haskell во имя добра!", с. 418) приводится
-- пример функции, которая вычисляет наибольший общий делитель с
-- помощью алгоритма Евклида, а также ее варианта, который ведет
-- протокол вычисления.
-- (gcd определен в Prelude, но не импортируется в текущий модуль.)
gcd :: Int -> Int -> Int
gcd a b
| b == 0 = a
| otherwise = gcd b (a `mod` b)
gcdLog1 :: Int -> Int -> Writer [String] Int
gcdLog1 a b
| b == 0 = do
tell ["Finished with " ++ show a]
return a
| otherwise = do
tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)]
gcdLog1 b (a `mod` b)
gcdVal1 :: Int -> Int -> Int
gcdVal1 a b = fst $ runWriter (gcdLog1 a b)
gcdMessage1 :: Int -> Int -> [String]
gcdMessage1 a b = snd $ runWriter (gcdLog1 a b)
-- > gcdVal 8 3
-- 1
-- > gcdMessage 8 3
-- ["8 mod 3 = 2","3 mod 2 = 1","2 mod 1 = 0","Finished with 1"]
-- Вместо моноида [String] можно было использовать моноид String.
-- В лекции 8 приводился моноид эндоморфизмов над некоторым типом a.
-- При a = String получаем моноид разностных строк с операцией композиции.
-- Его также можно использовать в монаде Writer, получив преимущество
-- в эффективности.
gcdLog2 :: Int -> Int -> Writer (Endo String) Int
gcdLog2 a b
| b == 0 = do
tell $ Endo $ showString "Finished with " . shows a
return a
| otherwise = do
tell $ Endo $ shows a . showString " mod " . shows b . showString " = "
. shows (a `mod` b) . showString ", "
gcdLog2 b (a `mod` b)
gcdVal2 :: Int -> Int -> Int
gcdVal2 a b = fst $ runWriter (gcdLog2 a b)
gcdMessage2 :: Int -> Int -> String
gcdMessage2 a b = appEndo (snd $ runWriter $ gcdLog2 a b) ""
-- В качестве последнего примера рассмотрим функцию showExp из домашнего
-- задания в лекции 7, которая возвращает текстовое представление
-- арифметического выражения.
data Exp =
Const Int
| Add Exp Exp
| Sub Exp Exp
| Mul Exp Exp
instance Num Exp where
(+) = Add
(*) = Mul
(-) = Sub
fromInteger = Const . fromInteger
abs = undefined
signum = undefined
e :: Exp
e = (5-3)*(1+2+4)
tellString :: String -> Writer (Endo String) ()
tellString = tell . diff -- = tell . Endo . (<>) = tell . Endo . (++)
tellChar :: Char -> Writer (Endo String) ()
tellChar = tell . Endo . (:)
write :: Exp -> Writer (Endo String) ()
write (Const n) = tellString $ show n
write (Add e1 e2) = do
tellChar '('
write e1
tellChar '+'
write e2
tellChar ')'
write (Sub e1 e2) = do
tellChar '('
write e1
tellChar '-'
write e2
tellChar ')'
write (Mul e1 e2) = do
tellChar '('
write e1
tellChar '*'
write e2
tellChar ')'
-- Как видно, эта программа очень похожа на императивную программу
-- с побочным эффектом, а именно с выводом.
showExp :: Exp -> String
showExp = (`appEndo` "") . snd . runWriter . write
-- Пример:
-- > showExp e
-- "((5-3)*((1+2)+4))"
-- Более подробно класс Monoid и его использование в монаде Writer
-- описаны в следующих книгах.
-- Липовача, М. Изучай Haskell во имя добра! ДМК Пресс, 2012. Глава 14.
-- Холомьёв А. Учебник по Haskell. 2012. С. 101-102, 112-116.