mirror of
https://github.com/qfpl/applied-fp-course.git
synced 2024-11-23 03:44:45 +03:00
Wording/formatting updates
This commit is contained in:
parent
3a8a44be9f
commit
dc42b2091f
@ -38,14 +38,13 @@ newtype HelloMsg = HelloMsg
|
||||
{ getHelloMsg :: ByteString }
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- The Conf type will need:
|
||||
-- The ``Conf`` type will need:
|
||||
-- - 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 think about might go wrong when trying to gather our configuration
|
||||
-- information.
|
||||
-- Similar to when we were considering what might go wrong with the RqType, lets
|
||||
-- think about might go wrong when trying to gather our config information.
|
||||
data ConfigError
|
||||
|
||||
-- Our application will be able to load configuration from both a file and
|
||||
@ -54,29 +53,29 @@ data ConfigError
|
||||
-- question will help us find which abstraction is correct for our needs...
|
||||
|
||||
-- We want the CommandLine 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:
|
||||
-- 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
|
||||
|
||||
-- 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:
|
||||
-- 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:
|
||||
|
||||
-- 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
|
||||
@ -86,7 +85,7 @@ defaultConf =
|
||||
error "defaultConf not implemented"
|
||||
|
||||
-- We need something that will take our PartialConf and see if can finally build
|
||||
-- a complete Conf record. Also we need to highlight any missing values by
|
||||
-- a complete ``Conf`` record. Also we need to highlight any missing values by
|
||||
-- providing the relevant error.
|
||||
makeConfig
|
||||
:: PartialConf
|
||||
@ -105,11 +104,11 @@ parseOptions =
|
||||
|
||||
-- | File Parsing
|
||||
|
||||
-- We're trying to avoid complications when selecting a configuration file package
|
||||
-- from Hackage. We'll use an encoding that you are probably familiar with, for
|
||||
-- better or worse, and write a small parser to pull out the bits we need. The package
|
||||
-- we're using is the 'aeson' package to parse some JSON and we'll pick the bits off
|
||||
-- the Object.
|
||||
-- We're trying to avoid complications when selecting a configuration file
|
||||
-- package from Hackage. We'll use an encoding that you are probably familiar
|
||||
-- with, for better or worse, and write a small parser to pull out the bits we
|
||||
-- need. The package we're using is the ``aeson`` package to parse some JSON and
|
||||
-- we'll pick the bits off the Object.
|
||||
|
||||
-- The documentation for this package will guide you in the right direction
|
||||
parseJSONConfigFile
|
||||
@ -120,13 +119,13 @@ parseJSONConfigFile =
|
||||
|
||||
-- | Command Line Parsing
|
||||
|
||||
-- We will use the 'optparse-applicative' package to build our command line parser.
|
||||
-- As this particular problem is fraught with silly dangers and we appreciate
|
||||
-- someone else having eaten this gremlin on our behalf.
|
||||
-- We will use the ``optparse-applicative`` package to build our command line
|
||||
-- parser. As this particular problem is fraught with silly dangers and we
|
||||
-- appreciate someone else having eaten this gremlin on our behalf.
|
||||
|
||||
-- You'll need to use the documentation for optparse-applicative to help you write
|
||||
-- these functions as we're relying on their API to produce the types we need. We've
|
||||
-- provided some of the less interesting boilerplate for you.
|
||||
-- You'll need to use the documentation for ``optparse-applicative`` to help you
|
||||
-- write these functions as we're relying on their API to produce the types we
|
||||
-- need. We've provided some of the less interesting boilerplate for you.
|
||||
commandLineParser
|
||||
:: ParserInfo PartialConf
|
||||
commandLineParser =
|
||||
|
@ -72,8 +72,8 @@ app cfg rq cb =
|
||||
handleRErr =
|
||||
either Left ( handleRequest cfg )
|
||||
|
||||
-- Now we have some config, we can pull our configured 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
|
||||
@ -99,7 +99,7 @@ mkRequest rq =
|
||||
-- List the current topics
|
||||
( ["list"], "GET" ) ->
|
||||
pure mkListRequest
|
||||
-- Finally we don't care about any other requests so throw your hands in the air
|
||||
-- Finally we don't care about any other requests so build an Error response
|
||||
_ ->
|
||||
pure mkUnknownRouteErr
|
||||
|
||||
|
@ -76,9 +76,9 @@ data ContentType
|
||||
= PlainText
|
||||
| JSON
|
||||
|
||||
-- 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.
|
||||
-- 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
|
||||
|
@ -145,7 +145,7 @@ parseJSONConfigFile fp = do
|
||||
|
||||
-- | Command Line Parsing
|
||||
|
||||
-- We will use the optparse-applicative package to build our command line
|
||||
-- We will use the ``optparse-applicative`` package to build our command line
|
||||
-- parser, as this problem is fraught with silly dangers and we appreciate
|
||||
-- someone else having eaten this gremlin on our behalf.
|
||||
commandLineParser
|
||||
|
@ -15,14 +15,12 @@ module FirstApp.Types
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Text (Text)
|
||||
|
||||
{-|
|
||||
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` 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.
|
||||
|
||||
The type system will check it for you, and the compiler will eliminate the cost
|
||||
once it has passed.
|
||||
-}
|
||||
-- The type system will check it for you, and the compiler will eliminate the
|
||||
-- cost once it has passed.
|
||||
newtype Topic = Topic Text
|
||||
deriving Show
|
||||
|
||||
@ -68,32 +66,29 @@ data RqType
|
||||
| ViewRq Topic
|
||||
| ListRq
|
||||
|
||||
{-|
|
||||
Not everything goes according to plan, but it's important that our
|
||||
types reflect when errors can be introduced into our program. Additionally
|
||||
it's useful to be able to be descriptive about what went wrong.
|
||||
-- Not everything goes according to plan, but it's important that our types
|
||||
-- reflect when errors can be introduced into our program. Additionally it's
|
||||
-- useful to be able to be descriptive about what went wrong.
|
||||
|
||||
So lets think about some of the basic things that can wrong with our
|
||||
program and create some values to represent that.
|
||||
-}
|
||||
-- So lets think about some of the basic things that can wrong with our program
|
||||
-- and create some values to represent that.
|
||||
data Error
|
||||
= UnknownRoute
|
||||
| EmptyCommentText
|
||||
| EmptyTopic
|
||||
deriving Show
|
||||
|
||||
-- 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/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.
|
||||
data ContentType
|
||||
= PlainText
|
||||
| JSON
|
||||
|
||||
-- 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.
|
||||
-- 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"
|
||||
renderContentType PlainText = "text/plain"
|
||||
renderContentType JSON = "application/json"
|
||||
|
@ -62,7 +62,8 @@ withTable =
|
||||
error "withTable not yet implemented"
|
||||
|
||||
-- Given a `FilePath` to our SQLite DB file, initialise the database and ensure
|
||||
-- our Table is there by running a query to create it, if it doesn't exist already.
|
||||
-- our Table is there by running a query to create it, if it doesn't exist
|
||||
-- already.
|
||||
initDb
|
||||
:: FilePath
|
||||
-> Table
|
||||
@ -76,9 +77,10 @@ initDb fp tab =
|
||||
createTableQ = withTable tab
|
||||
"CREATE TABLE IF NOT EXISTS $$tablename$$ (id INTEGER PRIMARY KEY, topic TEXT, comment TEXT, time INTEGER)"
|
||||
|
||||
-- Note that we don't store the Comment type in the DB, it is the type we build to
|
||||
-- send to the outside world. We will be loading our `DbComment` type from the
|
||||
-- FirstApp.DB.Types module before converting trying to convert it to a Comment.
|
||||
-- Note that we don't store the Comment type in the DB, it is the type we build
|
||||
-- to send to the outside world. We will be loading our `DbComment` type from
|
||||
-- the FirstApp.DB.Types module before converting trying to convert it to a
|
||||
-- Comment.
|
||||
getComments
|
||||
:: FirstAppDB
|
||||
-> Topic
|
||||
|
@ -37,10 +37,10 @@ module FirstApp.DB.PostgreSQL where
|
||||
-- , dbTable :: Table
|
||||
-- }
|
||||
|
||||
-- -- Unlike the sqlite-simple package, the postgresql-simple package allows for
|
||||
-- -- the specification of SQL identifiers for us in the Query construction. This
|
||||
-- -- has the advantage of not needing to worry about manually interpolating the
|
||||
-- -- table name into our queries. The package will safely handle this for us.
|
||||
-- Unlike the sqlite-simple package, the postgresql-simple package allows for
|
||||
-- the specification of SQL identifiers for us in the Query construction. This
|
||||
-- has the advantage of not needing to worry about manually interpolating the
|
||||
-- table name into our queries. The package will safely handle this for us.
|
||||
-- tableName
|
||||
-- :: FirstAppDB
|
||||
-- -> Identifier
|
||||
|
@ -39,11 +39,11 @@ newtype CommentText = CommentText Text
|
||||
deriving (Show, ToJSON)
|
||||
|
||||
-- This is the Comment record that we will be sending to users, it's a simple
|
||||
-- record type, containing an Int, Topic, CommentText, and UTCTime. However notice
|
||||
-- that we've also derived the Generic type class instance as well. This saves us
|
||||
-- some effort when it comes to creating encoding/decoding instances. Since our
|
||||
-- types are all simple types at the end of the day, we're able to let GHC do
|
||||
-- the work.
|
||||
-- record type, containing an Int, Topic, CommentText, and UTCTime. However
|
||||
-- notice that we've also derived the Generic type class instance as well. This
|
||||
-- saves us some effort when it comes to creating encoding/decoding instances.
|
||||
-- Since our types are all simple types at the end of the day, we're able to let
|
||||
-- GHC do the work.
|
||||
|
||||
newtype CommentId = CommentId Int
|
||||
deriving (Eq, Show, ToJSON)
|
||||
@ -57,10 +57,10 @@ data Comment = Comment
|
||||
deriving ( Show, Generic )
|
||||
|
||||
instance ToJSON Comment where
|
||||
-- This is one place where we can take advantage of our Generic instance. Aeson
|
||||
-- already has the encoding functions written for anything that implements
|
||||
-- the Generic typeclass. So we don't have to write our encoding, we ask
|
||||
-- Aeson to construct it for us.
|
||||
-- This is one place where we can take advantage of our Generic instance.
|
||||
-- Aeson already has the encoding functions written for anything that
|
||||
-- implements the Generic typeclass. So we don't have to write our encoding,
|
||||
-- we ask Aeson to construct it for us.
|
||||
toEncoding = A.genericToEncoding opts
|
||||
where
|
||||
-- These options let us make some minor adjustments to how Aeson treats
|
||||
@ -138,9 +138,9 @@ data ContentType
|
||||
= PlainText
|
||||
| JSON
|
||||
|
||||
-- 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.
|
||||
-- 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
|
||||
|
@ -29,7 +29,8 @@ data Env = Env
|
||||
}
|
||||
|
||||
-- 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.
|
||||
-- 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
|
||||
@ -38,12 +39,12 @@ newtype AppM a = AppM
|
||||
-- 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 less likely.
|
||||
--
|
||||
|
||||
-- Because we're using a newtype, all of the instance definitions for ReaderT
|
||||
-- would normally not apply. However, because we've done nothing but create a
|
||||
-- convenience wrapper for our ReaderT, it is not difficult for GHC to
|
||||
-- automatically derive instances on our behalf.
|
||||
--
|
||||
|
||||
-- With the 'GeneralizedNewtypeDeriving' pragma at the top of the file, we
|
||||
-- will be able to derive these instances automatically.
|
||||
{ unAppM :: ReaderT Env IO a }
|
||||
|
@ -48,8 +48,7 @@ closeDb =
|
||||
-- attempts to mitigate that somewhat by removing the need for repetitive string
|
||||
-- mangling when building our queries. We write the query and pass it through
|
||||
-- this function that requires the Table information and everything is taken
|
||||
-- care of for us. This is probably not the way to do things in a large scale
|
||||
-- app.
|
||||
-- care of for us. This is not the way to do things in a large scale program.
|
||||
withTable
|
||||
:: Table
|
||||
-> Query
|
||||
|
Loading…
Reference in New Issue
Block a user