-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathTransformers.hs
More file actions
712 lines (541 loc) · 19.8 KB
/
Transformers.hs
File metadata and controls
712 lines (541 loc) · 19.8 KB
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
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
{-
---
fulltitle: Monad Transformers
date: November 11, 2024
---
-}
module Transformers where
import Control.Monad (ap, liftM)
import Data.Function ((&))
import Data.Kind (Type)
import State (State)
import State qualified as S
{-
How do we use *multiple* monads at once?
========================================
Sometimes, you might need to both use a state transformer and signal
errors. We've seen that individual monads can structure our code nicely,
but what if we want to do more things at once? One solution is to
build a combined monad from scratch, like we saw in the parsing lecture.
Another solution is to use the *monad transformer* library (called `mtl`)
and build the combined monad from reusable components.
We'll demonstrate how that can work today, and discuss how that library is
designed, using a running example of simple *evaluators* for
a simple programming language.
Here is our simple language: only division expressions.
-}
data Expr
= Val Int
| Div Expr Expr
deriving (Show)
{-
Our first evaluator is *unsafe*. It could cause an error
when we run it.
-}
eval :: Expr -> Int
eval (Val n) = n
eval (Div x y) = eval x `div` eval y
{-
Here are two terms that we will use as running examples.
-}
ok :: Expr
ok =
(Val 1972 `Div` Val 2)
`Div` Val 23
err :: Expr
err =
Val 2
`Div` ( Val 1
`Div` (Val 2 `Div` Val 3)
)
{-
The first evaluates properly and returns a valid answer, while the
second fails with a divide-by-zero exception.
-}
-- >>> eval ok
-- >>> eval err
{-
This `eval` function is not very good because it can blow up with a
divide-by-zero error and stop the whole evaluator. And, there is nothing to
do about it: we can't catch the error.
Of course, one way to fix the problem is to detect the error and then
continue with a default value (such as 0).
-}
evalDefault :: Expr -> Int
evalDefault (Val n) = n
evalDefault (Div x y) =
let m = evalDefault y
in if m == 0 then 0 else evalDefault x `div` m
{-
But, no one likes this solution. It leads to buggy code.
Error Handling Via Exception Monads
-----------------------------------
Alternatively, we can use the `Either` type to treat the failure case more
gently: a `Left` result, carrying an error message, means that an error happened somewhere, while a
`Right n` result meant that evaluation succeeded yielding `n`.
We saw how this worked in the [`EitherMonad` exercise](08-monads/EitherMonad.hs).
Try to use the `Either` monad (with do notation) to write a better exception-
throwing evaluator, where the helper function `errorS` generates the error string.
-}
errorS :: (Show a) => a -> a -> String
errorS y m = "Error dividing " ++ show y ++ " by " ++ show m
-- | exception-throwing evaluator
evalEx :: Expr -> Either String Int
evalEx (Val n) = return n
evalEx (Div x y) = undefined
{-
When we call this evaluator, we'll format its result into a string using the
following function. For future flexibility, this function is
parameterized by a higher-order function used to display the result.
-}
showEx :: (a -> String) -> Either String a -> String
showEx _ (Left m) = "Raise: " ++ m
showEx s (Right v) = "Result: " ++ s v
{-
And this little function will call the evaluator above and then format
its result.
-}
goEx :: Expr -> String
goEx e =
evalEx e
& showEx show -- the `&` operator is reverse application
{-
This version should return `Result: 42` for the `ok` term and
`"Raise: Error dividing..."` for `err`.
-}
-- >>> goEx ok
-- "Result: 42"
-- >>> goEx err
-- "Raise: Error dividing Val 1 by Div (Val 2) (Val 3)"
{-
Counting Operations Using the State Monad
-----------------------------------------
Next, let's stop being paranoid about errors and instead try to do
some *profiling*. Let's imagine that the `div` operator is very
expensive, and that we would like to *count* the number of divisions
that are performed while evaluating a particular expression.
As you might imagine, our old friend the state monad is going to be
just what we need here! The type of store that we'd like to use is
just the count of number of division operations, and we can store that
in an `Int`.
-}
type Store = Int
{-
We'll need a way of incrementing the counter:
-}
tickProf :: State Store ()
tickProf = do
x <- S.get -- use get and put from the state monad
S.put (x + 1)
{-
Now we can write a *profiling* evaluator, and observe it at work.
-}
evalSt :: Expr -> State Store Int
evalSt (Val n) = return n
evalSt (Div x y) = do
m <- evalSt x
n <- evalSt y
tickProf
return (m `div` n)
{-
For flexibility later, we'll define a function that displays the result of
the State monad.
-}
-- | Show the result of runState, parameterized by a function
-- to show the value
showSt :: (a -> String) -> (a, Store) -> String
showSt f (v, cnt) = f v ++ ", count: " ++ show cnt
{-
And put it all together.
-}
goSt :: Expr -> String
goSt e =
evalSt e
& flip S.runState 0 -- The `&` operator is reverse application
& showSt show -- It lets us create a short transformation pipeline
-- >>> goSt ok
-- "42, count: 2"
{-
But... alas! To get the profiling, we threw out the nifty error
handling that we had put in earlier!!
-}
-- >>> goSt err
-- divide by zero
{-
Transformers: Making Monads Multitask
=====================================
So, at the moment, it seems that monads can do many things, but only *one
thing at a time* -- you can either use a monad to do the error- management
plumbing *or* to do the state-manipulation plumbing, but not both at the same
time. Is it too much ask for both? I guess we could write a
\*mega-state-and-exception* monad that supports the operations of both, but
that doesn't sound fun! Especially since, if we later decide to add yet
another feature, then we would have to make up yet another mega-monad.
So we will take a different approach, where we will keep *wrapping* --
or "decorating" -- monads with extra features, so that we can take a
simple monad, and then add the Exception monad's features to it, and
then add the State monad's features and so on.
The key to doing this is to define exception handling, state passing,
etc., not as monads, but rather as *type-level functions from monads
to monads.*
This will require a little more work up-front (most of which is done
already in well-designed libraries), but after that we can add new
features in a modular manner. For example, to get a mega
state-and-exception monad, we will start with a dummy `Identity`
monad, supply it to the `StateT` monad transformer (which yields
state-passing monad) and pass the result to the `ExceptT` monad
transformer, which yields the desired mega monad.
(Incidentally, if you are a Python programmer, the above may remind
some of you of the [Decorator Design Pattern][2] and other [Python
Decorators][3].)
Step 1: *Describing* Monads With Special Features
-----------------------------------------------
The first step to being able to compose monads is to define typeclasses that
describe monads *with* particular features. For example, the notion of an
\*exception monad* is captured by the typeclass that describes monads that are
also equipped with an appropriate `throwError` function.
-}
class (Monad m) => MonadError e m where
throwError :: e -> m a
{-
This function takes an error value of type `e` (e.g. `String` for error
messages). The result type is `m a` where `a` is polymorphic --- in
otherwords, we can throw an error in any (monadic) context.
We can make `Either s` an instance of the above class
like this:
-}
instance MonadError s (Either s) where
throwError :: s -> Either s a
throwError = Left
{-
Now see what happens if you change `Left` to `throwError` in the
evaluator `evalEx` above and remove the type signature.
What is the new type of the evaluator that GHC infers?
Similarly, we can bottle the key operations of a *state monad* in a typeclass
that describes monads equipped with extraction (get) and modification (put)
functions of appropriate types.
-}
class (Monad m) => MonadState s m where
get :: m s
put :: s -> m ()
{-
We can then redefine the ticking operation to work for any state monad:
-}
tickMonadState :: (MonadState Int m) => m ()
tickMonadState = do
(x :: Int) <- get
put (x + 1)
{-
Naturally, we can make our `State` monad an instance of the above:
-}
instance MonadState s (State s) where
get :: State s s
get = S.get
{-
>
-}
put :: s -> State s ()
put = S.put
{-
Now go back and see what happens when you replace `tickProf` with
`tickMonadState` in `evalSt` above and remove the type signature.
Step 2: Using Monads With Special Features
------------------------------------------
Armed with these two typeclasses, we can write our exception-throwing,
step-counting evaluator quite easily:
-}
evalMega :: (MonadError String m, MonadState Int m) => Expr -> m Int
evalMega (Val n) = return n
evalMega (Div x y) = do
n <- evalMega x
m <- evalMega y
if m == 0
then throwError $ errorS n m
else do
tickMonadState
return (n `div` m)
{-
Note that it is simply the combination of the two evaluators from before -- we
use the error handling from `evalEx` and the profiling from `evalSt`.
Meditate for a moment on the type of the above evaluator; note that it
works with *any monad* that is *both* a exception- and a state- monad!
(Also observe that if you remove this type annotation, GHC can infer it for you.)
Interlude: Creating MegaMonads
-----------------------------
But where do we *get* monads that are both state-manipulating and
exception-handling?
One answer is that we could just define one! Below is the start of the Mega
monad that I alluded to above: it's a bit fiddly to complete these definitions,
but you can generally use the types as your guide.
Next, finish the instances for `Monad`, `MonadError String`, and `MonadState
Int`. Make sure that `evalMega` works with your monad.
-}
newtype Mega a = Mega {runMega :: Int -> Either String (a, Int)}
instance Monad Mega where
return :: a -> Mega a
return x = undefined
(>>=) :: Mega a -> (a -> Mega b) -> Mega b
ma >>= fmb = undefined
instance Applicative Mega where
pure = return
(<*>) = ap
instance Functor Mega where
fmap = liftM
instance MonadError String Mega where
throwError :: String -> Mega a
throwError str = undefined
instance MonadState Int Mega where
get = undefined
put x = undefined
{-
Finally, once we have a Mega monad, we can run it and display the result.
-}
goMega :: Expr -> String
goMega e =
evalMega e
& flip runMega 0 -- reverse order of arguments
& showMega
where
showMega :: Either String (Int, Store) -> String
showMega = showEx (showSt show) -- use formatting functions from above
-- >>> goMega ok
-- "Result: 42, count: 2"
-- >>> goMega err
-- "Raise: Error dividing 1 by 0"
{-
In the end, making your own mega-monad is a bit disappointing, since we've
already defined the state- and exception-handling functionality separately.
A better answer is to build this monad (and others like it) piece by piece.
We'll do this by defining some type level functions that will *add* state
manipulation or exception handling to *any* pre-existing monad.
Step 3: Adding Features to Existing Monads
------------------------------------------
To add new features to existing monads, we use *monad transformers* --
type operators `t` that map a monad `m` to a new monad `t m`.
\**A Transformer For Exceptions**
Consider the following datatype declaration:
-}
type ExceptT :: Type -> (Type -> Type) -> Type -> Type
newtype ExceptT e m a = MkExc {runExceptT :: m (Either e a)}
{-
Look closely at the kind of `ExceptT` above.
This type constructor takes a type `e` (the type of the error value, such as
`String`), then an underlying monad `m` and then an argument `a`. It is a lot
like `Either e a` except that we have added a new monad `m` in the middle.
If you look at the definition of `ExceptT` you'll see that this monad *wraps*
the `Either e a` type.
If `m` is a monad, then we can make `ExceptT` a monad. Furthermore, the
instance below looks a lot like the Monad instance for the `Either` type above. We just need
to (a) work with the newtype (using `MkExc` and `runExceptT`) and (b) use return and
`>>=` from the monad `m` to glue computations together.
-}
instance (Monad m) => Monad (ExceptT e m) where
return :: forall a. a -> ExceptT e m a
return x = MkExc (return (Right x) :: m (Either e a))
(>>=) :: ExceptT e m a -> (a -> ExceptT e m b) -> ExceptT e m b
p >>= f =
MkExc $
runExceptT p
>>= ( \x -> case x of
Left e -> return (Left e)
Right a -> runExceptT (f a)
)
instance (Monad m) => Applicative (ExceptT e m) where
pure = return
(<*>) = ap
instance (Monad m) => Functor (ExceptT e m) where
fmap = liftM
{-
Next, we ensure that the transformer is also an exception monad by
equipping it with `throwError`.
Compare this definition to that of `MonadError e (Either e)` above.
-}
instance (Monad m) => MonadError e (ExceptT e m) where
throwError :: e -> ExceptT e m a
throwError msg = MkExc (return (Left msg))
{-
\**A Transformer For State**
Next, we will build a transformer for the state monad, following more or less
the same recipe as we did for exceptions. Here is the definition for the
transformer (preceded by its kind):
-}
type StateT :: Type -> (Type -> Type) -> Type -> Type
newtype StateT s m a = MkStateT {runStateT :: s -> m (a, s)}
{-
The enhanced monad is a variant of the ordinary state monad where a starting
store is mapped to a computation in the monad `m` that returns both a result
of type `a` and a new store.
(Aside: note that the monad transformer is *not* defined like this:
newtype StateT s m a = MkStateT { runStateT :: m (s -> (a, s)) }
That is, it is not an `m` computation yielding a store transformation.
Why is this not what we want?)
Next, we declare that the transformer's output is a monad. Again, compare the
definitions below to that of the `State` monad.
-}
instance (Monad m) => Monad (StateT s m) where
return :: a -> StateT s m a
return x = MkStateT $ \s -> return (x, s)
(>>=) :: StateT s m a -> (a -> StateT s m b) -> StateT s m b
p >>= f = MkStateT $ \s -> do
(r, s') <- runStateT p s
runStateT (f r) s'
instance (Monad m) => Applicative (StateT s m) where
pure = return
(<*>) = ap
instance (Monad m) => Functor (StateT s m) where
fmap = liftM
{-
And finally we declare that the transformer is a state monad by
equipping it with the operations from `MonadState Int`. You fill
in these definitions.
-}
instance (Monad m) => MonadState s (StateT s m) where
get :: StateT s m s
get = MkStateT getIt
where
getIt :: s -> m (s, s)
getIt s = undefined
put :: s -> StateT s m ()
put s = MkStateT putIt
where
putIt :: s -> m ((), s)
putIt _ = undefined
{-
Where are we now?
\* If m is a monad, then StateT s m is a state monad (i.e. an instance of MonadState)
\* If m is a monad, then ExceptT e m is an error monad (i.e. an instance of MonadError)
But, what about `StateT s (ExceptT e m)`? We know it is a state monad by the
above. But, we'd *also* like it to be an error monad.
In other words, we need the following "pass through" properties to hold:
\* If m is a *state* monad, then `ExceptT e` m is still a state monad
\* If m is an *error* monad, then `StateT Int` m is still an error monad
We can do this in a generic way.
Step 4: Preserving Old Features of Monads
------------------------------------------
Of course, we must make sure that the original features of the monads are not
lost in the transformed monads. The key ingredient of a transformer is that
it must have a function `lift` that takes an `m` operation and turns it into a
`t m` operation. For example, the `State` monad supports `get` and `set`. We
would like to be able to lift `get` and `set` so that they work for `ExceptT e
(State s)` too.
In general, this function will allow us to transfer operations from the old
monad into the transformed monad: any operation on the input monad `m` can be
directly lifted into the transformed monad, and so the transformation
\*preserves* all the operations of the original monad.
-}
class MonadTrans (t :: (Type -> Type) -> Type -> Type) where -- from Control.Monad.Trans (among other places)
lift :: (Monad m) => m a -> t m a
{-
It is easy to formally state that `ExceptT e` is a bona-fide transformer by
making it an instance of the `MonadTrans` class:
-}
instance MonadTrans (ExceptT e) where
lift :: (Monad m) => m a -> ExceptT e m a
-- Recall the type of MkExc
-- MkExc :: m (Either e a) -> ExceptT e m a
lift = MkExc . lift_
where
lift_ :: (Monad m) => m a -> m (Either e a)
lift_ mt = Right <$> mt
{-
Similarly, for the state monad transformer:
-}
instance MonadTrans (StateT s) where
lift :: (Monad m) => m a -> StateT s m a
-- Recall the type of MkStateT
-- MkStateT :: (s -> m (a,s)) -> StateT s m a
lift ma = MkStateT $ \s -> do
r <- ma
return (r, s)
{-
Using `lift`, we can ensure that, if a monad was already an
"error" monad, then the result of the state transformer is too:
-}
instance (MonadError e m) => MonadError e (StateT s m) where
throwError :: e -> StateT s m a
throwError = lift . throwError
{-
Similarly, if a monad was already a state-manipulating monad, then the
result of the exception-transformer is *also* a state-manipulating
monad:
-}
instance (MonadState s m) => MonadState s (ExceptT e m) where
get :: ExceptT e m s
get = lift get
put :: s -> ExceptT e m ()
put = lift . put
{-
Step 5: Whew! Put It Together and Run
-------------------------------------
Finally, we can put all the pieces together and run the transformers.
We can also *order* the transformations differently (which can have
different consequences on the output, as we will see).
Here are our two evaluators, distingiushed by the order of the transformers
that we use. Note that the code is the same in each case, but the
`MonadState` and `MonadError` constraints in `evalMega` are resolved
differently.
-}
evalExSt :: Expr -> StateT Int (Either String) Int
evalExSt = evalMega
evalStEx :: Expr -> ExceptT String (State Store) Int
evalStEx = evalMega
{-
And here are the two functions that format their results, defined with the
formatting functions from above.
-}
showExSt :: Either String (Int, Int) -> String
showExSt = showEx (showSt show)
showStEx :: (Either String Int, Int) -> String
showStEx = showSt (showEx show)
{-
We can run these interpreters and display their results as follows:
-}
goExSt :: Expr -> String
goExSt e =
(evalExSt e :: StateT Int (Either String) Int)
& flip runStateT 0
& showExSt
goStEx :: Expr -> String
goStEx e =
(evalStEx e :: ExceptT String (State Store) Int)
& runExceptT
& flip S.runState 0
& showStEx
{-
When everything works, we get the same answer. But look what happens if
we try to divide by zero!
-}
-- >>> goExSt ok
-- "Result: 42, count: 2"
-- >>> goExSt err
-- "Raise: Error dividing 1 by 0"
-- >>> goStEx ok
-- "Result: 42, count: 2"
-- >>> goStEx err
-- "Raise: Error dividing 1 by 0, count: 1"
{-
Step 6: Getting the original monads back
-----------------------------------------
It seems a little silly that the monad definitions for `State` and for
`StateT` share so much code. What if we want a monad that is *only* a state
monad, and not layered on top of something else?
As alluded to above, we can define an `Identity` monad to use under
any other.
-}
newtype Id a = MkId a deriving (Show)
instance Monad Id where
return x = undefined
(MkId p) >>= f = undefined
instance Applicative Id where
pure = return
(<*>) = ap
instance Functor Id where
fmap = liftM
type State2 s = StateT s Id -- isomorphic to State s
type Either2 s = ExceptT s Id -- isomorphic to Either s
{-
Step 7: Using the library in your code
---------------------------------------
The `mtl` library wraps contains the definitions of `StateT`, `MonadState`, `ExceptT`, `MonadError`, and the instances described above.
The file [MtlExample](MtlExample.hs) demonstrates how to use this library using the examples in this file.
-}