1
1
mirror of https://github.com/qfpl/applied-fp-course.git synced 2024-11-27 01:23:00 +03:00

Added more description around the ReaderT / AppM

More work needed to explain things a little better, not sure I've
made a good case for motivating the use of ReaderT or actually
explained it that well.
This commit is contained in:
Sean Chalmers 2017-08-17 11:31:46 +10:00
parent e06eae6695
commit 767256ebd4
3 changed files with 86 additions and 3 deletions

View File

@ -9,14 +9,50 @@ import Data.Text (Text)
import FirstApp.Conf (Conf)
import FirstApp.DB (FirstAppDB)
-- One motivation for using ReaderT is that there exists some information in
-- your application that is used almost everywhere and it is tiresome to have to
-- weave it through everywhere. We also don't allow "global variables" as they
-- are error prone, fragile, and prevent you from properly being able to reason
-- about the operation of your program.
--
-- a ReaderT is a function from some 'r' to some 'm a' : (r -> m a). Where by
-- the 'r' is accessible to all functions that run in the context of that 'm'.
--
-- This means that if you use the 'r' everywhere or simply enough throughout
-- your application, you no longer have to constantly weave the extra 'r' as an
-- argument to everything that might need it.
-- Since by definition:
-- foo :: ReaderT r m a
-- When run, becomes:
-- foo :: r -> m a
--
-- First, let's clean up our (Conf,FirstAppDB) with an application Env type. We
-- will add a general purpose logging function, since we're not limited to
-- just values!
data Env = Env
-- Add the type signature of a very general "logging" function.
{ loggingRick :: Text -> AppM ()
, envConfig :: Conf
, envDb :: FirstAppDB
}
-- Lets crack on and define a newtype wrapper for our ReaderT, this will save us
-- having to write out the full ReaderT definition for every function that uses it.
newtype AppM a = AppM
-- Our ReaderT will only contain the Env, and our base monad will be IO, leave
-- the return type polymorphic so that it will work regardless of what is
-- being returned from the functions that will use it. Using a newtype (in
-- addition to the useful type system) means that it is harder to use a
-- different ReaderT when we meant to use our own, or vice versa. In such a
-- situation it is extremely unlikely the application would compile at all,
-- but the name differences alone make the confusion a little less likely.
{ unAppM :: ReaderT Env IO a }
-- Because we're using a newtype, all of the instance definitions for ReaderT
-- would normally no apply. However, because we've done nothing but create a
-- convenience wrapper for our ReaderT, there is an extension for Haskell that
-- allows it to simply extend all the existing instances to work without AppM.
-- Add the GeneralizedNewtypeDeriving pragma to the top of the file and these
-- all work without any extra effort.
deriving ( Functor
, Applicative
, Monad
@ -24,6 +60,25 @@ newtype AppM a = AppM
, MonadIO
)
-- This a helper function that will take the requirements for our ReaderT, an
-- Env, and the (AppM a) that is the context/action to be run with the given Env.
--
-- First step is to unwrap our AppM, the newtype definition we wrote gives us
-- that function:
-- unAppM :: AppM a -> ReaderT Env IO a
--
-- Then we run the ReaderT, which itself is just a newtype to get access to the
-- action we're going to evaluate:
-- runReaderT :: ReaderT r m a -> r -> m a
-- ~
-- runReaderT :: ReaderT Env IO a -> Env -> IO a
--
-- Combining them (runReaderT . unAppM) we are left with:
-- Env -> IO a
--
-- We have an Env so that leaves us with the:
-- IO a
-- and we're done.
runAppM
:: Env
-> AppM a

View File

@ -89,8 +89,15 @@ app
app env rq cb =
requestToResponse >>= cb
where
requestToResponse = runAppM env $
mkRequest rq >>= handleRErr >>= handleRespErr
-- Now that our request handling and response creating functions operate
-- within our AppM context, we need to run the AppM to get our IO action out
-- to be run and handed off to the callback function. We've already written
-- the function for this so include the 'runAppM' with the Env.
requestToResponse = runAppM env $ do
-- Exercise: Rewrite this function to remove the need for the intermediate values.
rq' <- mkRequest rq
er' <- handleRErr rq'
handleRespErr er'
handleRespErr =
either mkErrorResponse pure
@ -102,14 +109,31 @@ handleRequest
:: RqType
-> AppM (Either Error Response)
handleRequest rqType = do
-- Now that we're operating within the context of our AppM, which is a
-- ReaderT, we're able to access the values stored in the Env.
--
-- Two functions that allow us to access the data stored in our ReaderT are:
-- ask :: MonadReader r m => m r
-- &
-- asks :: MonadReader r m => (r -> a) -> m a
--
-- We will use asks here as we're only after the FirstAppDB, so...
-- > envDb :: Env -> FirstAppDB
-- > AppM :: ReaderT Env IO a
-- > asks :: (Env -> a) -> AppM a
-- > asks envDb :: AppM FirstAppDB
db <- asks envDb
liftIO $ case rqType of
-- Exercise: Could this be generalised to clean up the repetition ?
AddRq t c -> fmap (const ( Res.resp200 "Success" )) <$> DB.addCommentToTopic db t c
ViewRq t -> fmap Res.resp200Json <$> DB.getComments db t
ListRq -> fmap Res.resp200Json <$> DB.getTopics db
mkRequest
:: Request
-- We change this to be in our AppM context as well because when we're
-- constructing our RqType we might want to call on settings or other such
-- things, maybe.
-> AppM ( Either Error RqType )
mkRequest rq =
case ( pathInfo rq, requestMethod rq ) of
@ -153,6 +177,9 @@ mkErrorResponse UnknownRoute = pure $ Res.resp404 "Unknown Route"
mkErrorResponse EmptyCommentText = pure $ Res.resp400 "Empty Comment"
mkErrorResponse EmptyTopic = pure $ Res.resp400 "Empty Topic"
mkErrorResponse ( DBError e ) = do
-- As with our request for the FirstAppDB, we use the asks function from
-- Control.Monad.Reader and pass the field accessor from the Env record.
rick <- asks loggingRick
rick . Text.pack $ show e
-- Be a sensible developer and don't leak your DB errors over the interwebs.
pure $ Res.resp500 "OH NOES"

View File

@ -17,6 +17,7 @@ main :: IO ()
main = do
let dieWith m = print m >> Exit.exitFailure
-- Keeping everything in sync with out larger application changes.
reqsE <- Main.prepareAppReqs
case reqsE of