1
1
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:
Sean Chalmers 2017-09-06 09:01:41 +10:00
parent 96a81b095a
commit 41cc4e931d
5 changed files with 66 additions and 64 deletions

View File

@ -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 =

View File

@ -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

View File

@ -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 =

View File

@ -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

View File

@ -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