From c9ccb0ec8de894c72a2da5be8ea43cea8fd0d6a5 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 19 Feb 2020 10:25:34 +1000 Subject: [PATCH 1/8] Tidy instructor notes --- INSTRUCTOR_NOTES.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/INSTRUCTOR_NOTES.md b/INSTRUCTOR_NOTES.md index 8d6fe9cb..451cdef7 100644 --- a/INSTRUCTOR_NOTES.md +++ b/INSTRUCTOR_NOTES.md @@ -7,7 +7,7 @@ We're going to be building an extremely simple web server using the 'wai' framework. Wai is a low level HTTP implementation similar to WSGI in Python or Rack in Ruby. -- Hackage +* Hackage - Primary repository for Haskell packages and their documentation. - Package **Index** is extremely useful for discovering and interrogating packages. @@ -27,17 +27,17 @@ Our application is starting to grow we should add another layer of assurance by writing some tests. We will cover: -- How to write a ``test-suite`` in the cabal file. -- Usage of the ``hspec`` library as our test runner and how to feed it our +* How to write a ``test-suite`` in the cabal file. +* Usage of the ``hspec`` library as our test runner and how to feed it our application. -- We will also write some tests using the ``hspec-wai`` package to give us some +* We will also write some tests using the ``hspec-wai`` package to give us some basic confidence in our error handling. # Level 4 This covers adding a database to our application. -- We use SQLite because it requires the least amount of setup on the host machine. -- All of the SQL is provided. +* We use SQLite because it requires the least amount of setup on the host machine. +* All of the SQL is provided. We will have the beginnings of some configuration in our application, but we will simply hardcode this for the time being as we will have a better solution @@ -63,12 +63,12 @@ The steps for this level: 3) ``src/Level04/DB.hs`` 4) ``src/Level04/Main.hs`` -- Call out `Traversable` and `Bifunctor` typeclasses. -- Call out the encoding instances & the automatic deriving of the ToJSON instances +* Call out `Traversable` and `Bifunctor` typeclasses. +* Call out the encoding instances & the automatic deriving of the ToJSON instances # Level 5 -This is the "ExceptT" level. +This is "The `ExceptT` level". After enduring some of the annoyance of manually handling the `Either` values in various ways. This level has the students implementing their own version of the `ExceptT` monad transformer. @@ -99,7 +99,7 @@ the file into a JSON Value. # Level 7 -This is "The ReaderT" level. +This is "The `ReaderT` level". Students will be required to copy their completed versions of functions from previous levels that will then break in this level and need to be refactored. @@ -113,15 +113,16 @@ Also there are functions in the FirstApp/Main module that will need to be updated to handle the new shenanigans. ## General Notes - More to add. -- readme in each level folder. -- monoid instance - rehash single/multiple number of possible instances. Don't let people hang too long on this point. -- how to find the documentation for the Header / ContentType -- more instruction that lead people to hackage documentation for Text/ByteString etc -- mention that import lists may need to be updated for the new types -- some editors will need to jump in and out of the different levels (close, cd, re-open) -- Mention that it's fine to use case statements for Either handling, we make it okay at the end. -- Mention that creating modules is easy, useful, and very helpful. +* readme in each level folder. +* monoid instance - rehash single/multiple number of possible instances. Don't let people hang too long on this point. +* how to find the documentation for the Header / ContentType +* more instruction that lead people to hackage documentation for Text/ByteString etc +* mention that import lists may need to be updated for the new types +* some editors will need to jump in and out of the different levels (close, cd, re-open) +* Mention that it's fine to use case statements for Either handling, we make it okay at the end. +* Mention that creating modules is easy, useful, and very helpful. ### IMPORTANT! -- Stephen Diehl - What I Wish I Knew Learning Haskell -- Ask students if they would prefer access to a prepared VM with the code & an editor or two. +* Stephen Diehl - [What I Wish I Knew Learning Haskell](http://dev.stephendiehl.com/hask/) +* Gabriel Gonzales - [State of the Haskell Ecosystem](https://github.com/Gabriel439/post-rfc/blob/master/sotu.md) +* Ask students if they would prefer access to a prepared VM with the code & an editor or two. From 09cae81b1b83b7a08dba63799db2b35f05e82e59 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 19 Feb 2020 18:30:15 +1000 Subject: [PATCH 2/8] Add bonus content - mtl --- README.md | 6 + bonus/README.md | 12 ++ bonus/mtl.md | 499 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 517 insertions(+) create mode 100644 bonus/README.md create mode 100644 bonus/mtl.md diff --git a/README.md b/README.md index 2d24c826..0ca42115 100644 --- a/README.md +++ b/README.md @@ -129,3 +129,9 @@ instructions about what the goal is for that specific level. * Level 09 : Add session controls (login, logout) and a protected route. So we can have something that resembles application state. For the purposes of modelling the state machine and implementing some property based tests. + +### Bonus Content + +Extension material that doesn't feel like it belongs to the main progression +lives in the [`bonus`](https://github.com/qfpl/applied-fp-course/tree/master/bonus) +subdirectory. diff --git a/bonus/README.md b/bonus/README.md new file mode 100644 index 00000000..db7f8a57 --- /dev/null +++ b/bonus/README.md @@ -0,0 +1,12 @@ +# Bonus Content + +This directory holds discussions that do not fit into the main +progression. They should make good reading for advanced students who +have completed the course, or as a reference for people taking their +knowledge onto real codebases. + +# Index + +* [`mtl.md`](https://github.com/qfpl/applied-fp-course/blob/master/bonus/mtl.md) - + Use the monad transformer library to build up an application monad + instead of writing everything by hand. diff --git a/bonus/mtl.md b/bonus/mtl.md new file mode 100644 index 00000000..40732031 --- /dev/null +++ b/bonus/mtl.md @@ -0,0 +1,499 @@ +# MTL - Monad Transformer Library + +This file is literate haskell. If you have `markdown-unlit` installed, +you can extract the code by running the following command: + +``` +$ markdown-unlit -h mtl.md mtl.md mtl.hs +``` + +This will allow you to work through the exercises using GHCi. + + +# Summary of the MTL Style + +Overall, "MTL Style" means the following things for a piece of +functionality `Foo` (e.g., `Except`, `Reader`, `State`, &c): + +1. A parcel of functionality bundled into a typeclasses `MonadFoo`; + +2. A monad transformer `FooT`, which provides the `MonadFoo` instance + which actually does the work; + +3. Instances `MonadTrans FooT` and `MonadIO m => MonadIO (FooT m)`; + +4. A type alias `type Foo = FooT Identity`, for when an underlying + monad is unnecessary; and + +5. Instances of the form `MonadFoo m => MonadFoo (BarT m)` which lift + `MonadFoo` functionality through other transformers. + + +# Building up to MTL + +
+Extensions and Imports +```haskell +{-# OPTIONS_GHC -Wall -Wno-unused-imports #-} + +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE UndecidableInstances #-} + +import Control.Applicative (liftA2) +import Data.Bifunctor (Bifunctor(..)) +import Data.Either (either) +``` +
+ +## Generalising `AppM` + +Consider the `AppM` monad we defined in level 6: + +```haskell +newtype AppM e a = AppM { runAppM :: IO (Either e a) } + +instance Functor (AppM e) where + fmap f = AppM . (fmap . fmap) f . runAppM + +instance Applicative (AppM e) where + pure = AppM . pure . pure + + AppM f <*> AppM a = AppM $ liftA2 (<*>) f a + +instance Monad (AppM e) where + AppM m >>= f = AppM $ m >>= either (pure . Left) (runAppM . f) +``` + +Let's look at this closely: + +1. The `Functor` instance is completely uninteresting. We could have + turned on `{-# LANGUAGE DeriveFunctor #-}` and written `deriving + Functor`, and GHC would generate the same code. + +2. The `Applicative` instance is slightly more interesting, but not + very. It is always fundamentally the same: composing two + `Applicative`s always yields an `Applicative`. Here, that's the + `IO` and `Either e` `Applicative`s, but this fact is witnessed in + general by the `instance (Applicative f, Applicative g) => + Applicative (Compose f g)` in + [`Data.Functor.Compose`](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor-Compose.html#t:Compose). + +3. The `Monad` instance is pattern-matching some `Either`-flavoued + stuff, and calling `(>>=)`. It is using no `IO`-specific features. + +This means that we can generalise the `AppM` instance over any +underlying monad: + +```haskell +-- Exercise: What are the constraints on the Functor instance? Check with `:i`. +newtype AppM' m e a = AppM' { runAppM' :: m (Either e a) } deriving Functor + +instance Applicative m => Applicative (AppM' m e) where + pure = AppM' . pure . pure + AppM' f <*> AppM' a = AppM'$ liftA2 (<*>) f a + +-- Exercise: Implement this +instance Monad m => Monad (AppM' m e) where + (>>=) :: AppM' m e a -> (a -> AppM' m e b) -> AppM' m e b + (>>=) = error "(>>=) -- AppM'" +``` + +
+Solution +```haskell ignore +instance Monad m => Monad (AppM' m e) where + AppM' m >>= f = AppM' $ m >>= either (pure . Left) (runAppM' . f) +``` +
+ + +## `AppM` is the Wrong Name + +At this point, we've found something very general, and it's no longer +appropriate to call it `AppM`. Changing the order of the type +variables, shows that we have rediscovered `ExceptT` from +`transformers`'s `Control.Monad.Trans.Except` module: + +```haskell +newtype ExceptT e m a = ExceptT { runExceptT :: m (Either e a) } + deriving Functor +``` + +
+Instances +```haskell +instance Applicative m => Applicative (ExceptT e m) where + pure = ExceptT . pure . pure + ExceptT f <*> ExceptT a = ExceptT $ liftA2 (<*>) f a + +instance Monad m => Monad (ExceptT e m) where + ExceptT m >>= f = ExceptT $ m >>= either (pure . Left) (runExceptT . f) +``` +
+ +`ExceptT e` is a _monad transformer_: if `m` is a monad, then so is +`ExceptT e m`. + + +## What if I don't want to Transform Anything? + +`mtl` provides aliases that use the `Identity` functor as the base monad: + +```haskell +-- From Data.Functor.Identity +newtype Identity a = Identity { runIdentity :: a } deriving Functor + +instance Applicative Identity where + pure = Identity + Identity f <*> Identity a = Identity $ f a + +instance Monad Identity where + Identity a >>= f = f a + +-- Type aliases are eta-reduced as far as possible, for maximum usefulness. +-- (GHC can't expand a type alias until it's fully applied.) +type Except e = ExceptT e Identity +type Reader r = ReaderT r Identity +``` + +## Other Transformers + + `transformers` provides a family of these transformers, and by +stacking them atop each other, you can build up a monad for your +needs. We will only talk about two: `ExceptT`, which we just +discovered; and `ReaderT`, which passes around an argument for +us. `ReaderT r` is a monad transformer: if `m` is a monad, `ReaderT r +m` is also a monad: + +```haskell +newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a } deriving Functor + +-- Excercise: Write the Applicative and Monad instances +instance Applicative m => Applicative (ReaderT r m) where + pure :: a -> ReaderT r m a + pure = error "pure -- ReaderT r m" + + (<*>) :: ReaderT r m (a -> b) -> ReaderT r m a -> ReaderT r m b + (<*>) = error "(<*>) -- ReaderT r m" + +instance Monad m => Monad (ReaderT r m) where + (>>=) :: ReaderT r m a -> (a -> ReaderT r m b) -> ReaderT r m b + (>>=) = error "(>>=) -- ReaderT r m" +``` + +
+Solution +```haskell ignore +instance Applicative m => Applicative (ReaderT r m) where + pure = ReaderT . pure . pure + ReaderT f <*> ReaderT a = ReaderT $ liftA2 (<*>) f a + +instance Monad m => Monad (ReaderT r m) where + ReaderT m >>= f = ReaderT $ \r -> do + a <- m r + runReaderT (f a) r +``` +
+ + +## Adding Features to Transformers + +Now that we have these general type definitions, we want to be able to +provide useful functions alongside them. + +```haskell +-- Return the environment. +ask :: Monad m => ReaderT r m r +ask = error "ask" + +-- Apply a function to the environment, and return it. +asks :: Monad m => (r -> a) -> ReaderT r m a +asks = error "asks" + +-- Run a subcomputation in a modified environment. +-- This is a specialisation of withReaderT. +local :: (r -> r) -> ReaderT r m a -> ReaderT r m a +local = error "local" + +-- Transform the environment of a reader. +withReaderT :: (r' -> r) -> ReaderT r m a -> ReaderT r' m a +withReaderT = error "withReaderT" + +throwError :: Applicative m => e -> ExceptT e m a +throwError = error "throwError" + +catchError :: Monad m => ExceptT e m a -> (e -> ExceptT e m a) -> ExceptT e m a +catchError = error "catchError" + +-- Transform the error type. +withExceptT :: Functor m => (e -> e') -> ExceptT e m a -> ExceptT e' m a +withExceptT = error "withExceptT" + +-- Transform the unwrapped computation. +mapExceptT + :: (m (Either e a) -> n (Either e' b)) -> ExceptT e m a -> ExceptT e' n b +mapExceptT = error "mapExceptT" + +-- Lift a "catchError"-shaped function through a ReaderT. +-- We will need this later when we introduce MTL typeclasses. +liftCatch + :: (m a -> (e -> m a) -> m a) + -> ReaderT r m a -> (e -> ReaderT r m a) -> ReaderT r m a +liftCatch = error "liftCatch" +``` + +
+Solution +```haskell ignore +ask :: Monad m => ReaderT r m r +ask = ReaderT pure + +asks :: Monad m => (r -> a) -> ReaderT r m a +asks f = f <$> ask + +local :: (r -> r) -> ReaderT r m a -> ReaderT r m a +local = withReaderT + +withReaderT :: (r' -> r) -> ReaderT r m a -> ReaderT r' m a +withReaderT f (ReaderT m) = ReaderT $ m . f + +mapExceptT + :: (m (Either e a) -> n (Either e' b)) -> ExceptT e m a -> ExceptT e' n b +mapExceptT f = ExceptT . f . runExceptT + +throwError :: Applicative m => e -> ExceptT e m a +throwError = ExceptT . pure . Left + +-- Notice how similar this is to the definition of (>>=) for ExceptT. +-- If this interests you, check out this paper: +-- "Exceptionally Monadic Error Handling" - https://arxiv.org/pdf/1810.13430.pdf +catchError :: Monad m => ExceptT e m a -> (e -> ExceptT e m a) -> ExceptT e m a +catchError (ExceptT m) f = ExceptT $ m >>= + either (runExceptT . f) (pure . Right) + +withExceptT :: Functor m => (e -> e') -> ExceptT e m a -> ExceptT e' m a +withExceptT f = ExceptT . fmap (first f) . runExceptT + +liftCatch + :: (m a -> (e -> m a) -> m a) + -> ReaderT r m a -> (e -> ReaderT r m a) -> ReaderT r m a +liftCatch catch (ReaderT m) f = ReaderT $ \r -> + catch (m r) (\e -> runReaderT (f e) r) +``` +
+ +## The `MonadTrans` Typeclass + +Now that we have this vocabulary of operations tied to each +transformer, it is often useful to be able to lift them through other +transformers, by ignoring the features added by the extra tranformers: + +```haskell +-- MonadTrans has the following laws, which show that `t` does indeed transform +-- `m`, and does so in a way that doesn't use the features of `t`. +-- +-- 1. lift . pure = pure +-- 2. lift (m >>= f) = lift m >>= lift . f +class MonadTrans t where + lift :: Monad m => m a -> t m a + +-- Exercise: Write MonadTrans instances for `ReaderT r` and `ExceptT e`. +instance MonadTrans (ReaderT r) where + lift :: m a -> ReaderT r m a + lift = error "lift -- ReaderT r m" + +instance MonadTrans (ExceptT e) where + lift :: m a -> ExceptT e m a + lift = error "lift -- ExceptT e m" +``` + +
+Solution +```haskell ignore +instance MonadTrans (ReaderT r) where + lift = ReaderT . const + +instance MonadTrans (ExceptT e) where + lift = ExceptT . fmap Right +``` +
+ +The `MonadIO` typeclass is a special case of this, which uses the fact +that there's no such thing as `IOT` to lift an `IO a` all the way up +the stack: + +```haskell +class Monad m => MonadIO m where + liftIO :: IO a -> m a + +-- Exercise: Write MonadIO instances for `ReaderT r m` and `ExceptT e m`, +-- assuming `MonadIO m`. Use `lift` instead of plumbing through the +-- transformers explicitly. +instance MonadIO m => MonadIO (ReaderT r m) where + liftIO :: IO a -> ReaderT r m a + liftIO = error "liftIO -- ReaderT r m" + +instance MonadIO m => MonadIO (ExceptT e m) where + liftIO :: IO a -> ExceptT r m a + liftIO = error "liftIO -- ExceptT r m" +``` + +
+Solution +```haskell ignore +instance MonadIO m => MonadIO (ReaderT r m) where + liftIO = lift . liftIO + +instance MonadIO m => MonadIO (ExceptT e m) where + liftIO = lift . liftIO +``` +
+ + +## Writing `lift` Everywhere Stinks! + +When you stack up a few transformers, it can get annoying writing +`lift` everywhere to lift operations from the lower transformers up +through the stack. Is there a better way? + +The answer is yes. The trick is to define typeclasses that do the +lifting for you, automatically. These are _multi-parameter +typeclasses_ (MPTCs). If you think of a normal typeclass as a +predicate over types, then an MPTC relates two or more types with each +other. An instance `MonadError e m` means that we can throw and catch +errors of type `e` in our monad `m`. + +The other syntax that may be new here is the `| m -> r`. This is +called a _functional dependency_ or "fundep" and tells GHC two things: + +1. There will never be two instances for `MonadReader r m` that have + the same `m` but different `r`. + +2. Because of this, if the typechecker determines `m`, it can + immediately conclude what `r` must be. + +The upside of this is that GHC's typechecker can automatically lift +operations through other transformers for you. The downside is that +you cannot have two `ExceptT` layers (say) in the same stack. If your +immediate response is to say "but I want to do that!", check out the +"classy MTL" pattern. + +```haskell +-- The functions in MTL do not have the trailing prime symbol (`'`), +-- but I must avoid duplicate definitions. +class Monad m => MonadReader r m | m -> r where + ask' :: m r + asks' :: (r -> a) -> m a + local' :: (r -> r) -> m a -> m a + +class Monad m => MonadError e m | m -> e where + throwError' :: e -> m a + catchError' :: m a -> (e -> m a) -> m a + +-- Exercise: write these four instances. +-- +-- The first two instances implement the class operations for each transformer. +-- You have written these functions already. +-- +-- The second two instances lift the functionality of one +-- transformer through another. +instance Monad m => MonadReader r (ReaderT r m) where + ask' = error "ask' -- ReaderT r m" + asks' = error "asks' -- ReaderT r m" + local' = error "local' -- ReaderT r m" + +instance Monad m => MonadError e (ExceptT e m) where + throwError' = error "throwError' -- ExceptT e m" + catchError' = error "catchError' -- ExceptT e m" + +instance MonadReader r m => MonadReader r (ExceptT e m) where + ask' = error "ask' -- lift through ExceptT" + asks' = error "asks' -- lift through ExceptT" + local' = error "local' -- lift through ExceptT" + +instance MonadError e m => MonadError e (ReaderT r m) where + throwError' = error "throwError' -- lift through ReaderT" + catchError' = error "catchError' -- lift through ReaderT" +``` + +
+Solution +```haskell ignore +instance Monad m => MonadReader r (ReaderT r m) where + ask' = ask + asks' = asks + local' = local + +instance Monad m => MonadError e (ExceptT e m) where + throwError' = throwError + catchError' = catchError + +instance MonadReader r m => MonadReader r (ExceptT e m) where + ask' = lift ask' + asks' = lift . asks' + local' = mapExceptT . local' + +instance MonadError e m => MonadError e (ReaderT r m) where + throwError' = lift . throwError' + catchError' = liftCatch catchError' +``` +
+ + +## Writing all the Instances Stinks! + +Yes. This is called the "O(n^2) instances" problem. Each bundle of +monad operations wants its own class, instances of that class for one +or more transformers (if you have multiple transformers providing +different implementations), plus instances that lift through every +other possible transformer. + +This is a lot of boilerplate, and it's important to know two things: + +1. It's not always possible to lift one instance through + another. Such instances do not and should not exist. + +2. Monad transformers, in general, do not commute. `StateT s (ExceptT + e m)` is a different beast to `ExceptT e (StateT s m)` - the former + throws away the state on error, while the latter preserves it. + +Algebraic effect systems attempt to cut down boilerplate, but make +compromises of their own. There is active research in this area. + + +## `AppM` Revisited + +The payoff of all this setup (helpfully encapsulated in the `mtl` +library, which builds atop `transformers`) is that we can specify our +application monad as a stack of transformers atop `IO`, and use GHC's +`GeneralizedNewtypeDeriving` extension to give us all the instances +immediately: + +```haskell +data Env -- Dummy type for the sake of example + +-- This is the Level07 AppM, which also passes around an environment +newtype AppMtl e a = AppMtl (ReaderT Env (ExceptT e IO) a) + deriving (Functor, Applicative, Monad, MonadError e, MonadReader Env) + +runAppMtl :: AppMtl e a -> Env -> IO (Either e a) +runAppMtl (AppMtl m) env = runExceptT $ runReaderT m env +``` + + +## A note on Perfomance + +Monad transformers are a great way to rapidly set up application +monads, and a well-placed `runFooT` call can let you temporarily pick +up extra functionality where it makes sense. Sometimes, GHC is not +smart enough to inline all the dictionary passing, and this may slow +down your program. + +Should this happen to you (and be proven by profiling), consider +defining your application monad directly and only then implementing +the `MonadFoo` instances by hand. From fc3395053d3dbca15ddcb692039dfeb57ce5ff27 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 19 Feb 2020 18:33:37 +1000 Subject: [PATCH 3/8] Fix IRC link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ca42115..24941c55 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ These lessons are designed to be completed with an instructor as part of the Data61 Applied Functional Programming Course. You are of course welcome to clone the repository and give it a try, but you may find the tasks more difficult. If you have any questions we can be contacted in the -Freenode [#qfpl or #fp-course IRC channel](https://freenode.net). You can use the +Freenode [#qfpl IRC channel](https://freenode.net). You can use the free [WebChat client](https://webchat.freenode.net). #### Subsequent lessons may contain spoilers, don't cheat yourself out of the experience! From 7d8628171e06ca9a7dd3c0a939870cee0dd4f117 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 19 Feb 2020 18:34:51 +1000 Subject: [PATCH 4/8] Add spacing - maybe it fixes github rendering? --- bonus/mtl.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bonus/mtl.md b/bonus/mtl.md index 40732031..837e4715 100644 --- a/bonus/mtl.md +++ b/bonus/mtl.md @@ -33,6 +33,7 @@ functionality `Foo` (e.g., `Except`, `Reader`, `State`, &c):
Extensions and Imports + ```haskell {-# OPTIONS_GHC -Wall -Wno-unused-imports #-} @@ -48,6 +49,7 @@ import Control.Applicative (liftA2) import Data.Bifunctor (Bifunctor(..)) import Data.Either (either) ``` +
## Generalising `AppM` @@ -105,10 +107,12 @@ instance Monad m => Monad (AppM' m e) where
Solution + ```haskell ignore instance Monad m => Monad (AppM' m e) where AppM' m >>= f = AppM' $ m >>= either (pure . Left) (runAppM' . f) ``` +
@@ -126,6 +130,7 @@ newtype ExceptT e m a = ExceptT { runExceptT :: m (Either e a) }
Instances + ```haskell instance Applicative m => Applicative (ExceptT e m) where pure = ExceptT . pure . pure @@ -134,6 +139,7 @@ instance Applicative m => Applicative (ExceptT e m) where instance Monad m => Monad (ExceptT e m) where ExceptT m >>= f = ExceptT $ m >>= either (pure . Left) (runExceptT . f) ``` +
`ExceptT e` is a _monad transformer_: if `m` is a monad, then so is @@ -188,6 +194,7 @@ instance Monad m => Monad (ReaderT r m) where
Solution + ```haskell ignore instance Applicative m => Applicative (ReaderT r m) where pure = ReaderT . pure . pure @@ -198,6 +205,7 @@ instance Monad m => Monad (ReaderT r m) where a <- m r runReaderT (f a) r ``` +
@@ -249,6 +257,7 @@ liftCatch = error "liftCatch"
Solution + ```haskell ignore ask :: Monad m => ReaderT r m r ask = ReaderT pure @@ -285,6 +294,7 @@ liftCatch liftCatch catch (ReaderT m) f = ReaderT $ \r -> catch (m r) (\e -> runReaderT (f e) r) ``` +
## The `MonadTrans` Typeclass @@ -314,6 +324,7 @@ instance MonadTrans (ExceptT e) where
Solution + ```haskell ignore instance MonadTrans (ReaderT r) where lift = ReaderT . const @@ -321,6 +332,7 @@ instance MonadTrans (ReaderT r) where instance MonadTrans (ExceptT e) where lift = ExceptT . fmap Right ``` +
The `MonadIO` typeclass is a special case of this, which uses the fact @@ -345,6 +357,7 @@ instance MonadIO m => MonadIO (ExceptT e m) where
Solution + ```haskell ignore instance MonadIO m => MonadIO (ReaderT r m) where liftIO = lift . liftIO @@ -352,6 +365,7 @@ instance MonadIO m => MonadIO (ReaderT r m) where instance MonadIO m => MonadIO (ExceptT e m) where liftIO = lift . liftIO ``` +
@@ -423,6 +437,7 @@ instance MonadError e m => MonadError e (ReaderT r m) where
Solution + ```haskell ignore instance Monad m => MonadReader r (ReaderT r m) where ask' = ask @@ -442,6 +457,7 @@ instance MonadError e m => MonadError e (ReaderT r m) where throwError' = lift . throwError' catchError' = liftCatch catchError' ``` +
From c256a915f8d629e9b3a7645bd83e52fe9eb102cd Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 19 Feb 2020 18:39:03 +1000 Subject: [PATCH 5/8] Nitpicks, add link to markdown-unlit --- bonus/mtl.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bonus/mtl.md b/bonus/mtl.md index 837e4715..99785f3e 100644 --- a/bonus/mtl.md +++ b/bonus/mtl.md @@ -1,7 +1,8 @@ # MTL - Monad Transformer Library -This file is literate haskell. If you have `markdown-unlit` installed, -you can extract the code by running the following command: +This file is literate haskell. If you have +[`markdown-unlit`](https://hackage.haskell.org/package/markdown-unlit) +installed, you can extract the code by running the following command: ``` $ markdown-unlit -h mtl.md mtl.md mtl.hs @@ -10,10 +11,10 @@ $ markdown-unlit -h mtl.md mtl.md mtl.hs This will allow you to work through the exercises using GHCi. -# Summary of the MTL Style +# Summary of MTL Style -Overall, "MTL Style" means the following things for a piece of -functionality `Foo` (e.g., `Except`, `Reader`, `State`, &c): +"MTL Style" means the following things for a piece of functionality +`Foo` (e.g., `Except`, `Reader`, `State`, &c): 1. A parcel of functionality bundled into a typeclasses `MonadFoo`; @@ -413,9 +414,6 @@ class Monad m => MonadError e m | m -> e where -- -- The first two instances implement the class operations for each transformer. -- You have written these functions already. --- --- The second two instances lift the functionality of one --- transformer through another. instance Monad m => MonadReader r (ReaderT r m) where ask' = error "ask' -- ReaderT r m" asks' = error "asks' -- ReaderT r m" @@ -425,6 +423,8 @@ instance Monad m => MonadError e (ExceptT e m) where throwError' = error "throwError' -- ExceptT e m" catchError' = error "catchError' -- ExceptT e m" +-- These two instances lift the functionality of one +-- transformer through another. instance MonadReader r m => MonadReader r (ExceptT e m) where ask' = error "ask' -- lift through ExceptT" asks' = error "asks' -- lift through ExceptT" @@ -502,7 +502,7 @@ runAppMtl (AppMtl m) env = runExceptT $ runReaderT m env ``` -## A note on Perfomance +## A Note on Perfomance Monad transformers are a great way to rapidly set up application monads, and a well-placed `runFooT` call can let you temporarily pick From fc4c8347d0e3eb32a261fa2f821a042c9a4bcd55 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 20 Feb 2020 10:26:54 +1000 Subject: [PATCH 6/8] Incorporate feedback --- bonus/mtl.md | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/bonus/mtl.md b/bonus/mtl.md index 99785f3e..6b7cc878 100644 --- a/bonus/mtl.md +++ b/bonus/mtl.md @@ -11,20 +11,40 @@ $ markdown-unlit -h mtl.md mtl.md mtl.hs This will allow you to work through the exercises using GHCi. -# Summary of MTL Style +# The Payoff -"MTL Style" means the following things for a piece of functionality -`Foo` (e.g., `Except`, `Reader`, `State`, &c): +MTL allows you to build an application monad by layering independent +pieces of functionality (called "transformers") atop a base monad +(usually `IO`). -1. A parcel of functionality bundled into a typeclasses `MonadFoo`; +```haskell ignore +data Env = Env -- Dummy type for the sake of example + +-- This is the Level07 AppM, which also passes around an environment +-- (Look ma, no handwritten instances!) +newtype AppM e a = AppM (ReaderT Env (ExceptT e IO) a) + deriving (Functor, Applicative, Monad, MonadError e, MonadReader Env) + +runAppM :: AppM e a -> Env -> IO (Either e a) +runAppM (AppM m) env = runExceptT $ runReaderT m env +``` + + +# Summary of an MTL-Style API + +"MTL Style" means the following things are usually available for a +piece of functionality `Foo` (e.g., `Except`, `Reader`, `State`, &c): + +1. A parcel of functionality bundled into a typeclass (usually called + `MonadFoo`); 2. A monad transformer `FooT`, which provides the `MonadFoo` instance which actually does the work; 3. Instances `MonadTrans FooT` and `MonadIO m => MonadIO (FooT m)`; -4. A type alias `type Foo = FooT Identity`, for when an underlying - monad is unnecessary; and +4. If the transformer can be usefully applied to `Identity`, a type + alias `type Foo = FooT Identity`; and 5. Instances of the form `MonadFoo m => MonadFoo (BarT m)` which lift `MonadFoo` functionality through other transformers. @@ -491,14 +511,15 @@ application monad as a stack of transformers atop `IO`, and use GHC's immediately: ```haskell -data Env -- Dummy type for the sake of example +data Env = Env -- Dummy type for the sake of example -- This is the Level07 AppM, which also passes around an environment -newtype AppMtl e a = AppMtl (ReaderT Env (ExceptT e IO) a) +-- (Look ma, no handwritten instances!) +newtype AppM e a = AppM (ReaderT Env (ExceptT e IO) a) deriving (Functor, Applicative, Monad, MonadError e, MonadReader Env) -runAppMtl :: AppMtl e a -> Env -> IO (Either e a) -runAppMtl (AppMtl m) env = runExceptT $ runReaderT m env +runAppM :: AppM e a -> Env -> IO (Either e a) +runAppM (AppM m) env = runExceptT $ runReaderT m env ``` @@ -513,3 +534,6 @@ down your program. Should this happen to you (and be proven by profiling), consider defining your application monad directly and only then implementing the `MonadFoo` instances by hand. + +As with any performance advice, profile before and after to make sure +you're actually achieving something. From 44b655ccdf0d95a8e33353de951fcdbf8ca91cda Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 20 Feb 2020 10:30:12 +1000 Subject: [PATCH 7/8] Clarify wording --- bonus/mtl.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bonus/mtl.md b/bonus/mtl.md index 6b7cc878..e8c612c0 100644 --- a/bonus/mtl.md +++ b/bonus/mtl.md @@ -109,8 +109,7 @@ Let's look at this closely: 3. The `Monad` instance is pattern-matching some `Either`-flavoued stuff, and calling `(>>=)`. It is using no `IO`-specific features. -This means that we can generalise the `AppM` instance over any -underlying monad: +This means that we can generalise `AppM` over any underlying monad: ```haskell -- Exercise: What are the constraints on the Functor instance? Check with `:i`. From 05e739e94afbe6dc990c0aa1bc4f2b27f400421a Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Thu, 20 Feb 2020 13:54:40 +1000 Subject: [PATCH 8/8] Final nits --- bonus/mtl.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bonus/mtl.md b/bonus/mtl.md index e8c612c0..3ac3d403 100644 --- a/bonus/mtl.md +++ b/bonus/mtl.md @@ -140,7 +140,7 @@ instance Monad m => Monad (AppM' m e) where At this point, we've found something very general, and it's no longer appropriate to call it `AppM`. Changing the order of the type -variables, shows that we have rediscovered `ExceptT` from +variables shows that we have rediscovered `ExceptT` from `transformers`'s `Control.Monad.Trans.Except` module: ```haskell @@ -193,8 +193,8 @@ type Reader r = ReaderT r Identity stacking them atop each other, you can build up a monad for your needs. We will only talk about two: `ExceptT`, which we just discovered; and `ReaderT`, which passes around an argument for -us. `ReaderT r` is a monad transformer: if `m` is a monad, `ReaderT r -m` is also a monad: +us. `ReaderT r` is another monad transformer: if `m` is a monad, +`ReaderT r m` is also a monad: ```haskell newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a } deriving Functor @@ -235,6 +235,8 @@ Now that we have these general type definitions, we want to be able to provide useful functions alongside them. ```haskell +-- Exercise: implement all of these. + -- Return the environment. ask :: Monad m => ReaderT r m r ask = error "ask" @@ -325,7 +327,7 @@ transformers, by ignoring the features added by the extra tranformers: ```haskell -- MonadTrans has the following laws, which show that `t` does indeed transform --- `m`, and does so in a way that doesn't use the features of `t`. +-- `m`, and does so in a way that doesn't use the features of `t`: -- -- 1. lift . pure = pure -- 2. lift (m >>= f) = lift m >>= lift . f @@ -403,7 +405,7 @@ other. An instance `MonadError e m` means that we can throw and catch errors of type `e` in our monad `m`. The other syntax that may be new here is the `| m -> r`. This is -called a _functional dependency_ or "fundep" and tells GHC two things: +called a _functional dependency_ or "fundep", and tells GHC two things: 1. There will never be two instances for `MonadReader r m` that have the same `m` but different `r`.