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

Merge pull request #88 from qfpl/semigroup-last

Last from Monoid is deprecated, use the one from Semigroup
This commit is contained in:
Sean Chalmers 2019-09-19 12:56:56 +10:00 committed by GitHub
commit e0832881cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 78 deletions

View File

@ -2,7 +2,7 @@ module Level06.Conf.CommandLine
( commandLineParser
) where
import Data.Monoid (Last (Last), (<>))
import Data.Semigroup (Last (Last), (<>))
import Options.Applicative (Parser, eitherReader, execParser,
fullDesc, header, help, helper, info,
@ -11,7 +11,7 @@ import Options.Applicative (Parser, eitherReader, execParser,
import Text.Read (readEither)
import Level06.Types (DBFilePath (DBFilePath),
import Level06.Types (DBFilePath (DBFilePath),
PartialConf (PartialConf), Port (Port))
-- | Command Line Parsing
@ -36,7 +36,7 @@ partialConfParser =
-- Parse the Port value off the command line args and into a Last wrapper.
portParser
:: Parser (Last Port)
:: Parser (Maybe (Last Port))
portParser =
let
mods = long "port"
@ -46,11 +46,11 @@ portParser =
-- A custom parser to turn a String into a Word16, before putting it into a Port
portReader = eitherReader (fmap Port . readEither)
in
Last <$> optional (option portReader mods)
fmap Last <$> optional (option portReader mods)
-- Parse the DBFilePath from the input string into our type and into a Last wrapper.
dbFilePathParser
:: Parser (Last DBFilePath)
:: Parser (Maybe (Last DBFilePath))
dbFilePathParser =
let
mods = long "db-filepath"
@ -58,4 +58,4 @@ dbFilePathParser =
<> metavar "DBFILEPATH"
<> help "File path for our SQLite Database file."
in
Last <$> optional (DBFilePath <$> strOption mods)
fmap Last <$> optional (DBFilePath <$> strOption mods)

View File

@ -36,9 +36,7 @@ import Data.Text (Text, pack)
import System.IO.Error (IOError)
import Data.Monoid (Last (..),
Monoid (mappend, mempty))
import Data.Semigroup (Semigroup ((<>)))
import Data.Semigroup (Last (..), Semigroup ((<>)))
import Data.Functor.Contravariant ((>$<))
import Data.List (stripPrefix)
@ -172,8 +170,8 @@ confPortToWai
confPortToWai =
error "confPortToWai not implemented"
-- Similar to when we were considering our application types. We can add to this sum type as we
-- build our application and the compiler can help us out.
-- Similar to when we were considering our application types. We can add to this sum type
-- as we build our application and the compiler can help us out.
data ConfigError
= BadConfFile DecodeError
deriving Show
@ -189,40 +187,31 @@ data ConfigError
-- ``defaults <> file <> commandLine``
-- We can use the ``Monoid`` typeclass to handle combining the ``Conf`` records
-- We can use the ``Semigroup`` typeclass to handle combining the ``Conf`` records
-- 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.
-- ``Semigroup`` instance will always preference the last value that it has:
--
-- Just (Last 3) <> Just (Last 1) = Just (Last 1)
-- Nothing <> Just (Last 1) = Just (Last 1)
-- Just (Last 1) <> Nothing = Just (Last 1)
--
-- To make this easier, we'll make a new type ``PartialConf`` that will have our ``Last``
-- wrapped values. We can then define a ``Semigroup`` instance for it and have our
-- ``Conf`` be a known good configuration.
data PartialConf = PartialConf
{ pcPort :: Last Port
, pcDBFilePath :: Last DBFilePath
{ pcPort :: Maybe (Last Port)
, pcDBFilePath :: Maybe (Last DBFilePath)
}
-- Before we can define our ``Monoid`` instance for ``PartialConf``, we'll have
-- to define a Semigroup instance. We define our ``(<>)`` function to lean
-- on the ``Semigroup`` instance for Last to always get the last value.
-- We need to define a ``Semigroup`` instance for ``PartialConf``. We define our ``(<>)``
-- function to lean on the ``Semigroup`` instance for Last to always get the last value.
instance Semigroup PartialConf where
_a <> _b = PartialConf
{ pcPort = error "pcPort (<>) not implemented"
, pcDBFilePath = error "pcDBFilePath (<>) not implemented"
}
-- We now define our ``Monoid`` instance for ``PartialConf``. Allowing us to
-- define our always empty configuration, which would always fail our
-- requirements. We just define `mappend` to be an alias of ``(<>)``
instance Monoid PartialConf where
mempty = PartialConf mempty mempty
mappend = (<>)
-- | When it comes to reading the configuration options from the command-line, we
-- use the 'optparse-applicative' package. This part of the exercise has already
-- been completed for you, feel free to have a look through the 'CommandLine'

View File

@ -7,7 +7,7 @@ module Level07.Conf
import GHC.Word (Word16)
import Data.Bifunctor (first)
import Data.Monoid (Last (..), (<>))
import Data.Semigroup (Last (..), (<>))
import Level07.Types (Conf (..), ConfigError (..),
DBFilePath (DBFilePath),
@ -21,8 +21,8 @@ import Level07.Conf.File (parseJSONConfigFile)
defaultConf
:: PartialConf
defaultConf = PartialConf
(pure (Port 3000))
(pure (DBFilePath "app_db.db"))
(pure (Last $ Port 3000))
(pure (Last $ DBFilePath "app_db.db"))
-- 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 config values
@ -39,10 +39,10 @@ makeConfig pc = Conf
-- like to be explicit in your intentions.
lastToEither
:: ConfigError
-> (PartialConf -> Last b)
-> (PartialConf -> Maybe (Last b))
-> Either ConfigError b
lastToEither e g =
(maybe (Left e) Right . getLast . g) pc
maybe (Left e) (Right . getLast) . g $ pc
-- This is the function we'll actually export for building our configuration.
-- Since it wraps all our efforts to read information from the command line, and

View File

@ -2,7 +2,7 @@ module Level07.Conf.CommandLine
( commandLineParser
) where
import Data.Monoid (Last (Last), (<>))
import Data.Semigroup (Last (Last), (<>))
import Options.Applicative (Parser, eitherReader, execParser,
fullDesc, header, help, helper, info,
@ -11,7 +11,7 @@ import Options.Applicative (Parser, eitherReader, execParser,
import Text.Read (readEither)
import Level07.Types (DBFilePath (DBFilePath),
import Level07.Types (DBFilePath (DBFilePath),
PartialConf (PartialConf), Port (Port))
-- | Command Line Parsing
@ -36,7 +36,7 @@ partialConfParser =
-- Parse the Port value off the command line args and into a Last wrapper.
portParser
:: Parser (Last Port)
:: Parser (Maybe (Last Port))
portParser =
let
mods = long "port"
@ -46,11 +46,11 @@ portParser =
-- A custom parser to turn a String into a Word16, before putting it into a Port
portReader = eitherReader (fmap Port . readEither)
in
Last <$> optional (option portReader mods)
fmap Last <$> optional (option portReader mods)
-- Parse the DBFilePath from the input string into our type and into a Last wrapper.
dbFilePathParser
:: Parser (Last DBFilePath)
:: Parser (Maybe (Last DBFilePath))
dbFilePathParser =
let
mods = long "db-filepath"
@ -58,4 +58,4 @@ dbFilePathParser =
<> metavar "DBFILEPATH"
<> help "File path for our SQLite Database file."
in
Last <$> optional (DBFilePath <$> strOption mods)
fmap Last <$> optional (DBFilePath <$> strOption mods)

View File

@ -32,8 +32,7 @@ import Data.ByteString (ByteString)
import Data.Text (pack)
import Data.Functor.Contravariant ((>$<))
import Data.Monoid (Last (Last))
import Data.Semigroup (Semigroup ((<>)))
import Data.Semigroup (Last (Last), Semigroup ((<>)))
import Data.Time (UTCTime)
import qualified Data.Time.Format as TF
@ -162,51 +161,42 @@ data ConfigError
| ConfigFileReadError IOError
deriving Show
-- 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 do we combine the different
-- inputs to enable this property?
-- 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 do we combine the different inputs to enable this
-- property?
-- 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:
-- 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``
-- We can use the ``Monoid`` typeclass to handle combining the ``Conf`` records
-- We can use the ``Semigroup`` typeclass to handle combining the ``Conf`` records
-- 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.
-- ``Semigroup`` instance will always preference the last value that it has:
--
-- Just (Last 3) <> Just (Last 1) = Just (Last 1)
-- Nothing <> Just (Last 1) = Just (Last 1)
-- Just (Last 1) <> Nothing = Just (Last 1)
--
-- To make this easier, we'll make a new type ``PartialConf`` that will have our ``Last``
-- wrapped values. We can then define a ``Semigroup`` instance for it and have our
-- ``Conf`` be a known good configuration.
data PartialConf = PartialConf
{ pcPort :: Last Port
, pcDBFilePath :: Last DBFilePath
{ pcPort :: Maybe (Last Port)
, pcDBFilePath :: Maybe (Last DBFilePath)
}
-- Before we can define our ``Monoid`` instance for ``PartialConf``, we'll have
-- to define a Semigroup instance. We define our ``(<>)`` function to lean
-- on the ``Semigroup`` instance for Last to always get the last value.
-- We need to define a ``Semigroup`` instance for ``PartialConf``. We define our ``(<>)``
-- function to lean on the ``Semigroup`` instance for Last to always get the last value.
instance Semigroup PartialConf where
a <> b = PartialConf
{ pcPort = pcPort a <> pcPort b
, pcDBFilePath = pcDBFilePath a <> pcDBFilePath b
}
-- We now define our ``Monoid`` instance for ``PartialConf``. Allowing us to
-- define our always empty configuration, which would always fail our
-- requirements. We just define `mappend` to be an alias of ``(<>)``
instance Monoid PartialConf where
mempty = PartialConf mempty mempty
mappend = (<>)
-- When it comes to reading the configuration options from the command-line, we
-- use the 'optparse-applicative' package. This part of the exercise has already
-- been completed for you, feel free to have a look through the 'CommandLine'
@ -221,7 +211,7 @@ partialConfDecoder = PartialConf
<$> lastAt "port" D.integral Port
<*> lastAt "dbFilePath" D.string DBFilePath
where
lastAt k d c = Last . fmap c <$> D.atKeyOptional k d
lastAt k d c = fmap (Last . c) <$> D.atKeyOptional k d
-- We have a data type to simplify passing around the information we need to run
-- our database queries. This also allows things to change over time without