mirror of
https://github.com/qfpl/applied-fp-course.git
synced 2024-11-23 03:44:45 +03:00
Wording updates
Removed some comments. Rephrased some other sections. Made it more obvious when I was referring to actual types.
This commit is contained in:
parent
96a81b095a
commit
41cc4e931d
@ -52,9 +52,10 @@ resp400 =
|
||||
error "resp400 not implemented"
|
||||
-- |
|
||||
|
||||
-- These helpers will take the raw request information and turn it into
|
||||
-- one of our data types. This means we draw a line about where the unruly outside
|
||||
-- world must end, and where the well-typed world of our application begins.
|
||||
-- These next few functions will take the raw request information and turn it
|
||||
-- into one of our data types. This means we draw a line about where the unruly
|
||||
-- outside world must end, and where the well-typed world of our application
|
||||
-- begins.
|
||||
mkAddRequest
|
||||
:: Text
|
||||
-> LBS.ByteString
|
||||
@ -62,9 +63,10 @@ mkAddRequest
|
||||
mkAddRequest =
|
||||
error "mkAddRequest not implemented"
|
||||
|
||||
-- This has other benefits, we're able isolate our validation requirements into the
|
||||
-- smallest chunks we can manage. This allows for fantastic reuse and it also means
|
||||
-- that validation is not spread across the application. It is kept at the borders.
|
||||
-- This has other benefits, we're able isolate our validation requirements into
|
||||
-- the smallest chunks we can manage. This allows for fantastic reuse and it
|
||||
-- also means that validation is not spread across the application. It is kept
|
||||
-- at the borders.
|
||||
mkViewRequest
|
||||
:: Text
|
||||
-> Either Error RqType
|
||||
@ -73,7 +75,7 @@ mkViewRequest =
|
||||
|
||||
-- Even though it may seem too trivial or even pointless to write functions such
|
||||
-- as these it allows for much greater consistency across the application.
|
||||
--
|
||||
|
||||
-- Some of these are straight forward data constructors, but by doing it this
|
||||
-- way we don't have any snowflakes littered about the code. It also enhances
|
||||
-- our ability to spot larger patterns in our application, which are
|
||||
@ -94,24 +96,24 @@ mkErrorResponse
|
||||
mkErrorResponse =
|
||||
error "mkErrorResponse not implemented"
|
||||
|
||||
{-|
|
||||
Lets use our RqTypes to write a function that will take the input from the
|
||||
Wai library and turn it into something our application cares about.
|
||||
-}
|
||||
|
||||
-- Lets use our ``RqType`` helpers to now write a function that will take the
|
||||
-- input ``Request`` from the Wai library and turn it into something our
|
||||
-- application cares about.
|
||||
mkRequest
|
||||
:: Request
|
||||
-> IO ( Either Error RqType )
|
||||
mkRequest =
|
||||
error "mkRequest not implemented"
|
||||
|
||||
-- Notice how we're only accepting our predefined request types that have the
|
||||
-- required information already validated and prepared for use in the handling
|
||||
-- of the request.
|
||||
-- In this next function, notice how we're only accepting our predefined request
|
||||
-- types that have the required information already validated and prepared for
|
||||
-- use in the handling of the request.
|
||||
--
|
||||
-- If we find that we need more information to handle a request, or we have a
|
||||
-- new type of request that we'd like to handle then we update the RqType
|
||||
-- structure and the compiler will let us know the affected portions of our
|
||||
-- application.
|
||||
-- new type of request that we'd like to handle then we update the ``RqType``
|
||||
-- structure and the compiler will let us know which parts of our application
|
||||
-- are affected.
|
||||
--
|
||||
-- Reduction of concerns such that each section of the application only deals
|
||||
-- with a small piece is one of the benefits of developing in this way.
|
||||
@ -121,8 +123,8 @@ handleRequest
|
||||
handleRequest _ =
|
||||
error "handleRequest not implemented"
|
||||
|
||||
-- Reimplement our `app` function using the new functions and the RqTypes as a
|
||||
-- guide.
|
||||
-- Reimplement this function using the new functions and the ``RqType``
|
||||
-- constructors as a guide.
|
||||
app
|
||||
:: Application
|
||||
app =
|
||||
|
@ -35,35 +35,33 @@ data RqType = RqType
|
||||
-- it's useful to be able to be descriptive about what went wrong.
|
||||
|
||||
-- Think about some of the basic things that can wrong with our Requests and
|
||||
-- building the RqTypes, and create some values to represent that. For now we
|
||||
-- don't need to worry about things like malformed requests or invalid headers
|
||||
-- etc.
|
||||
-- constructing a ``RqType``, and create some values to represent that. For now
|
||||
-- we don't need to worry about things like malformed requests or invalid
|
||||
-- headers etc.
|
||||
data Error = Error
|
||||
|
||||
-- Provide a type to list our response content types so we don't try to
|
||||
-- do the wrong thing with what we meant to be used as text or JSON etc.
|
||||
-- Provide a type to list our response content types so we don't try to do the
|
||||
-- wrong thing with what we meant to be used as text or JSON etc.
|
||||
data ContentType = ContentType
|
||||
|
||||
-- The ContentType description for a header doesn't match our data definition
|
||||
-- so we write a little helper function to pattern match on our ContentType
|
||||
-- value and provide the correct header value.
|
||||
|
||||
renderContentType
|
||||
:: ContentType
|
||||
-> ByteString
|
||||
renderContentType =
|
||||
error "renderContentType not implemented"
|
||||
|
||||
-- In Haskell the `newtype` comes with zero runtime cost. It is purely used for
|
||||
-- type-checking. So when you have a bare 'primitive' value, like an Int, String, or
|
||||
-- even [a], you can wrap it up in a `newtype` for clarity.
|
||||
-- In Haskell the ``newtype`` is a wrapper of sorts that comes with zero runtime
|
||||
-- cost. It is purely used for type-checking. So when you have a bare primitive
|
||||
-- value, like an ``Int``, ``String``, or even ``[a]``, you can wrap it up in a
|
||||
-- ``newtype`` to give it a descriptive name to be more precise in your types.
|
||||
|
||||
-- The type system will check it for you, and the compiler will eliminate the cost.
|
||||
-- The type system will check it for you, and the compiler will eliminate the
|
||||
-- cost of the "wrapper". Also, having specialised constructor functions for the
|
||||
-- newtypes allows you to set extra restrictions, such as minimum values.
|
||||
|
||||
-- Having specialised constructor functions for the newtypes allows you to set
|
||||
-- extra restrictions for your newtype.
|
||||
|
||||
-- Write two `newtype` definitions for `Topic` and `CommentText` that wrap a
|
||||
-- `Text` value
|
||||
-- We've constructed the ``newtype`` definitions for ``Topic`` and
|
||||
-- ``CommentText`` below.
|
||||
|
||||
-- Topic
|
||||
newtype Topic = Topic Text
|
||||
@ -74,9 +72,10 @@ newtype CommentText = CommentText Text
|
||||
deriving Show
|
||||
-- |
|
||||
|
||||
-- A benefit of `newtype` is that we can choose to *not* export the constructor
|
||||
-- and provide a function of our own. In our case, we're not interested in empty
|
||||
-- `Text` values so we will eliminate them and immediately report an error.
|
||||
-- We can choose to *not* export the constructor for a data type and instead
|
||||
-- provide a function of our own. In our case, we're not interested in empty
|
||||
-- `Text` values so we will eliminate them with a special constructor and return
|
||||
-- an error if an empty input is provided.
|
||||
mkTopic
|
||||
:: Text
|
||||
-> Either Error Topic
|
||||
|
@ -39,8 +39,8 @@ newtype HelloMsg = HelloMsg
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- The ``Conf`` type will need:
|
||||
-- - A customisable port number: `Port`
|
||||
-- - A changeable message for our users: `HelloMsg`
|
||||
-- - A customisable port number: ``Port``
|
||||
-- - A changeable message for our users: ``HelloMsg``
|
||||
data Conf = Conf
|
||||
|
||||
-- Similar to when we were considering what might go wrong with the RqType, lets
|
||||
@ -49,33 +49,34 @@ data ConfigError
|
||||
|
||||
-- Our application will be able to load configuration from both a file and
|
||||
-- command line input. We want to be able to use the command line to temporarily
|
||||
-- override the configuration from our file. How to would you combine them? This
|
||||
-- question will help us find which abstraction is correct for our needs...
|
||||
-- override the configuration from our file. How do we combine the different
|
||||
-- inputs to enable this property?
|
||||
|
||||
-- We want the CommandLine configuration to take precedence over the File
|
||||
-- We want the command line configuration to take precedence over the File
|
||||
-- configuration, so if we think about combining each of our ``Conf`` records,
|
||||
-- we want to be able to write something like this:
|
||||
|
||||
-- defaults <> file <> commandLine
|
||||
-- ``defaults <> file <> commandLine``
|
||||
|
||||
-- We can use the ``Monoid`` typeclass to handle combining the ``Conf`` records
|
||||
-- together, and the Last newtype to wrap up our values to handle the desired
|
||||
-- precedence. The Last newtype is a wrapper for Maybe that when used with its
|
||||
-- ``Monoid`` instance will always preference the last Just value that it has:
|
||||
-- together, and the ``Last`` type to wrap up our values to handle the desired
|
||||
-- precedence. The ``Last`` type is a wrapper for Maybe that when used with its
|
||||
-- ``Monoid`` instance will always preference the last ``Just`` value that it
|
||||
-- has:
|
||||
|
||||
-- Last (Just 3) <> Last (Just 1) = Last (Just 1)
|
||||
-- Last Nothing <> Last (Just 1) = Last (Just 1)
|
||||
-- Last (Just 1) <> Last Nothing = Last (Just 1)
|
||||
|
||||
-- To make this easier, we'll make a new type `PartialConf` that will have our
|
||||
-- Last wrapped values. We can then define a ``Monoid`` instance for it and have
|
||||
-- our ``Conf`` be a known good configuration.
|
||||
-- To make this easier, we'll make a new type ``PartialConf`` that will have our
|
||||
-- ``Last`` wrapped values. We can then define a ``Monoid`` instance for it and
|
||||
-- have our ``Conf`` be a known good configuration.
|
||||
data PartialConf
|
||||
|
||||
-- We now define our ``Monoid`` instance for PartialConf. Allowing us to define
|
||||
-- our always empty configuration, which would always fail our requirements.
|
||||
-- More interestingly, we define our ``mappend`` function to lean on the
|
||||
-- ``Monoid`` instance for Last to always get the last value.
|
||||
-- We now define our ``Monoid`` instance for ``PartialConf``. Allowing us to
|
||||
-- define our always empty configuration, which would always fail our
|
||||
-- requirements. More interestingly, we define our ``mappend`` function to lean
|
||||
-- on the ``Monoid`` instance for Last to always get the last value.
|
||||
instance Monoid PartialConf where
|
||||
|
||||
-- Set some sane defaults that we can always rely on
|
||||
@ -127,8 +128,8 @@ fromJsonObjWithKey
|
||||
fromJsonObjWithKey =
|
||||
error "fromJsonObjWithKey not implemented"
|
||||
|
||||
-- Construct the function that will take a FilePath, read it in and attempt to
|
||||
-- decode it as a valid JSON object, using the ``aeson`` package. Then pull
|
||||
-- Construct the function that will take a ``FilePath``, read it in and attempt
|
||||
-- to decode it as a valid JSON object, using the ``aeson`` package. Then pull
|
||||
-- specific keys off this object and construct our ``PartialConf``. Using the
|
||||
-- function we wrote above to assist in pulling items off the object.
|
||||
parseJSONConfigFile
|
||||
@ -161,7 +162,7 @@ commandLineParser =
|
||||
in
|
||||
info (helper <*> partialConfParser) mods
|
||||
|
||||
-- Combine the smaller parsers into our larger PartialConf type.
|
||||
-- Combine the smaller parsers into our larger ``PartialConf`` type.
|
||||
partialConfParser
|
||||
:: Parser PartialConf
|
||||
partialConfParser =
|
||||
|
@ -20,9 +20,11 @@ import FirstApp.Types
|
||||
|
||||
runApp :: IO ()
|
||||
runApp = do
|
||||
-- Load up the configuration by providing a FilePath for the JSON config file.
|
||||
-- Load up the configuration by providing a ``FilePath`` for the JSON config
|
||||
-- file.
|
||||
cfgE <- error "configuration not implemented"
|
||||
-- Loading the configuration can fail, so we have to take that into account now.
|
||||
-- Loading the configuration can fail, so we have to take that into account
|
||||
-- now.
|
||||
case cfgE of
|
||||
Left err -> undefined
|
||||
Right _cfg -> run undefined undefined
|
||||
@ -72,8 +74,8 @@ app cfg rq cb =
|
||||
handleRErr =
|
||||
either Left ( handleRequest cfg )
|
||||
|
||||
-- Now we have some config, we can pull the helloMsg off it and use it in the
|
||||
-- response.
|
||||
-- Now we have some config, we can pull the ``helloMsg`` off it and use it in
|
||||
-- the response.
|
||||
handleRequest
|
||||
:: a
|
||||
-> RqType
|
||||
|
@ -21,8 +21,6 @@ newtype Topic = Topic Text
|
||||
newtype CommentText = CommentText Text
|
||||
deriving Show
|
||||
|
||||
-- Having specialised constructor functions for the newtypes allows you to set
|
||||
-- restrictions for your newtype.
|
||||
mkTopic
|
||||
:: Text
|
||||
-> Either Error Topic
|
||||
|
Loading…
Reference in New Issue
Block a user