lib: Remove non-law-abiding Monoid instance for Journal.

This commit is contained in:
Stephen Morgan 2020-02-29 20:54:24 +11:00 committed by Simon Michael
parent 702c958487
commit e0dde6fe57
7 changed files with 46 additions and 49 deletions

View File

@ -90,6 +90,7 @@ import Control.Monad.Extra
import Control.Monad.Reader as R import Control.Monad.Reader as R
import Control.Monad.ST import Control.Monad.ST
import Data.Array.ST import Data.Array.ST
import Data.Default (Default(..))
import Data.Function ((&)) import Data.Function ((&))
import qualified Data.HashTable.ST.Cuckoo as H import qualified Data.HashTable.ST.Cuckoo as H
import Data.List import Data.List
@ -97,9 +98,8 @@ import Data.List.Extra (groupSort, nubSort)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe import Data.Maybe
#if !(MIN_VERSION_base(4,11,0)) #if !(MIN_VERSION_base(4,11,0))
import Data.Monoid import Data.Semigroup (Semigroup(..))
#endif #endif
import qualified Data.Semigroup as Sem
import qualified Data.Set as S import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
@ -157,7 +157,7 @@ instance Show Journal where
-- ,show $ map fst $ jfiles j -- ,show $ map fst $ jfiles j
-- ] -- ]
-- The monoid instance for Journal is useful for two situations. -- The semigroup instance for Journal is useful for two situations.
-- --
-- 1. concatenating finalised journals, eg with multiple -f options: -- 1. concatenating finalised journals, eg with multiple -f options:
-- FIRST <> SECOND. The second's list fields are appended to the -- FIRST <> SECOND. The second's list fields are appended to the
@ -168,7 +168,9 @@ instance Show Journal where
-- CHILD <> PARENT. A parsed journal's data is in reverse order, so -- CHILD <> PARENT. A parsed journal's data is in reverse order, so
-- this gives what we want. -- this gives what we want.
-- --
instance Sem.Semigroup Journal where -- Note that (<>) is right-biased, so nulljournal is only a left identity.
-- In particular, this prevents Journal from being a monoid.
instance Semigroup Journal where
j1 <> j2 = Journal { j1 <> j2 = Journal {
jparsedefaultyear = jparsedefaultyear j2 jparsedefaultyear = jparsedefaultyear j2
,jparsedefaultcommodity = jparsedefaultcommodity j2 ,jparsedefaultcommodity = jparsedefaultcommodity j2
@ -190,12 +192,8 @@ instance Sem.Semigroup Journal where
,jlastreadtime = max (jlastreadtime j1) (jlastreadtime j2) ,jlastreadtime = max (jlastreadtime j1) (jlastreadtime j2)
} }
instance Monoid Journal where instance Default Journal where
mempty = nulljournal def = nulljournal
#if !(MIN_VERSION_base(4,11,0))
-- This is redundant starting with base-4.11 / GHC 8.4.
mappend = (Sem.<>)
#endif
nulljournal :: Journal nulljournal :: Journal
nulljournal = Journal { nulljournal = Journal {

View File

@ -48,22 +48,24 @@ import Control.Arrow (right)
import qualified Control.Exception as C import qualified Control.Exception as C
import Control.Monad (when) import Control.Monad (when)
import "mtl" Control.Monad.Except (runExceptT) import "mtl" Control.Monad.Except (runExceptT)
import Data.Default import Data.Default (def)
import Data.Foldable (asum) import Data.Foldable (asum)
import Data.List import Data.List (group, sort, sortBy)
import Data.Maybe import Data.List.NonEmpty (nonEmpty)
import Data.Ord import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import Data.Semigroup (sconcat)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time (Day) import Data.Time (Day)
import Safe import Safe (headDef)
import System.Directory (doesFileExist, getHomeDirectory) import System.Directory (doesFileExist, getHomeDirectory)
import System.Environment (getEnv) import System.Environment (getEnv)
import System.Exit (exitFailure) import System.Exit (exitFailure)
import System.FilePath import System.FilePath ((<.>), (</>), splitDirectories, splitFileName)
import System.Info (os) import System.Info (os)
import System.IO import System.IO (stderr, writeFile)
import Text.Printf import Text.Printf (hPrintf, printf)
import Hledger.Data.Dates (getCurrentDay, parsedate, showDate) import Hledger.Data.Dates (getCurrentDay, parsedate, showDate)
import Hledger.Data.Types import Hledger.Data.Types
@ -150,11 +152,7 @@ type PrefixedFilePath = FilePath
-- Also the final parse state saved in the Journal does span all files. -- Also the final parse state saved in the Journal does span all files.
readJournalFiles :: InputOpts -> [PrefixedFilePath] -> IO (Either String Journal) readJournalFiles :: InputOpts -> [PrefixedFilePath] -> IO (Either String Journal)
readJournalFiles iopts = readJournalFiles iopts =
(right mconcat1 . sequence <$>) . mapM (readJournalFile iopts) fmap (right (maybe def sconcat . nonEmpty) . sequence) . mapM (readJournalFile iopts)
where
mconcat1 :: Monoid t => [t] -> t
mconcat1 [] = mempty
mconcat1 x = foldr1 mappend x
-- | Read a Journal from this file, or from stdin if the file path is -, -- | Read a Journal from this file, or from stdin if the file path is -,
-- or return an error message. The file path can have a READER: prefix. -- or return an error message. The file path can have a READER: prefix.

View File

@ -222,7 +222,7 @@ rtp = runTextParser
runJournalParser, rjp runJournalParser, rjp
:: Monad m :: Monad m
=> JournalParser m a -> Text -> m (Either (ParseErrorBundle Text CustomErr) a) => JournalParser m a -> Text -> m (Either (ParseErrorBundle Text CustomErr) a)
runJournalParser p t = runParserT (evalStateT p mempty) "" t runJournalParser p t = runParserT (evalStateT p nulljournal) "" t
rjp = runJournalParser rjp = runJournalParser
-- | Run an erroring journal parser in some monad. See also: parseWithState. -- | Run an erroring journal parser in some monad. See also: parseWithState.
@ -232,7 +232,7 @@ runErroringJournalParser, rejp
-> Text -> Text
-> m (Either FinalParseError (Either (ParseErrorBundle Text CustomErr) a)) -> m (Either FinalParseError (Either (ParseErrorBundle Text CustomErr) a))
runErroringJournalParser p t = runErroringJournalParser p t =
runExceptT $ runParserT (evalStateT p mempty) "" t runExceptT $ runParserT (evalStateT p nulljournal) "" t
rejp = runErroringJournalParser rejp = runErroringJournalParser
genericSourcePos :: SourcePos -> GenericSourcePos genericSourcePos :: SourcePos -> GenericSourcePos
@ -680,7 +680,7 @@ amountwithoutpricep = do
-- | Parse an amount from a string, or get an error. -- | Parse an amount from a string, or get an error.
amountp' :: String -> Amount amountp' :: String -> Amount
amountp' s = amountp' s =
case runParser (evalStateT (amountp <* eof) mempty) "" (T.pack s) of case runParser (evalStateT (amountp <* eof) nulljournal) "" (T.pack s) of
Right amt -> amt Right amt -> amt
Left err -> error' $ show err -- XXX should throwError Left err -> error' $ show err -- XXX should throwError

View File

@ -963,7 +963,7 @@ mkPosting rules record number accountFld amountFld amountInFld amountOutFld bala
| all isSpace str = Nothing | all isSpace str = Nothing
| otherwise = Just | otherwise = Just
(either (balanceerror n str) id $ (either (balanceerror n str) id $
runParser (evalStateT (amountp <* eof) mempty) "" $ runParser (evalStateT (amountp <* eof) nulljournal) "" $
T.pack $ (currency++) $ simplifySign str T.pack $ (currency++) $ simplifySign str
,nullsourcepos) -- XXX parse position to show when assertion fails, ,nullsourcepos) -- XXX parse position to show when assertion fails,
-- the csv record's line number would be good -- the csv record's line number would be good
@ -1039,7 +1039,7 @@ chooseAmount rules record currency amountFld amountInFld amountOutFld =
parseAmount currency amountstr = parseAmount currency amountstr =
either (amounterror amountstr) (Mixed . (:[])) either (amounterror amountstr) (Mixed . (:[]))
<$> runParser (evalStateT (amountp <* eof) mempty) "" <$> runParser (evalStateT (amountp <* eof) nulljournal) ""
<$> T.pack <$> T.pack
<$> (currency++) <$> (currency++)
<$> simplifySign <$> simplifySign

View File

@ -85,7 +85,7 @@ import Control.Monad.Trans.Class (lift)
import Data.Either (isRight) import Data.Either (isRight)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
#if !(MIN_VERSION_base(4,11,0)) #if !(MIN_VERSION_base(4,11,0))
import Data.Monoid ((<>)) import Data.Semigroup ((<>))
#endif #endif
import Data.Text (Text) import Data.Text (Text)
import Data.String import Data.String
@ -298,7 +298,7 @@ includedirectivep = do
put $ updatedChildj <> parentj put $ updatedChildj <> parentj
newJournalWithParseStateFrom :: FilePath -> Journal -> Journal newJournalWithParseStateFrom :: FilePath -> Journal -> Journal
newJournalWithParseStateFrom filepath j = mempty{ newJournalWithParseStateFrom filepath j = nulljournal{
jparsedefaultyear = jparsedefaultyear j jparsedefaultyear = jparsedefaultyear j
,jparsedefaultcommodity = jparsedefaultcommodity j ,jparsedefaultcommodity = jparsedefaultcommodity j
,jparseparentaccounts = jparseparentaccounts j ,jparseparentaccounts = jparseparentaccounts j
@ -747,7 +747,7 @@ tests_JournalReader = tests "JournalReader" [
,test "yearless date with no default year" $ assertParseError datep "1/1" "current year is unknown" ,test "yearless date with no default year" $ assertParseError datep "1/1" "current year is unknown"
,test "yearless date with default year" $ do ,test "yearless date with default year" $ do
let s = "1/1" let s = "1/1"
ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep s ep <- parseWithState nulljournal{jparsedefaultyear=Just 2018} datep s
either (assertFailure . ("parse error at "++) . customErrorBundlePretty) (const $ return ()) ep either (assertFailure . ("parse error at "++) . customErrorBundlePretty) (const $ return ()) ep
,test "no leading zero" $ assertParse datep "2018/1/1" ,test "no leading zero" $ assertParse datep "2018/1/1"
] ]

View File

@ -25,8 +25,9 @@ where
import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.State.Strict (StateT, evalStateT, execStateT) import Control.Monad.State.Strict (StateT, evalStateT, execStateT)
import Data.Default (Default(..))
#if !(MIN_VERSION_base(4,11,0)) #if !(MIN_VERSION_base(4,11,0))
import Data.Monoid ((<>)) import Data.Semigroup ((<>))
#endif #endif
-- import Data.CallStack -- import Data.CallStack
import Data.List (isInfixOf) import Data.List (isInfixOf)
@ -73,35 +74,35 @@ assertRight (Left a) = assertFailure $ "expected Right, got (Left " ++ show a +
-- | Assert that this stateful parser runnable in IO successfully parses -- | Assert that this stateful parser runnable in IO successfully parses
-- all of the given input text, showing the parse error if it fails. -- all of the given input text, showing the parse error if it fails.
-- Suitable for hledger's JournalParser parsers. -- Suitable for hledger's JournalParser parsers.
assertParse :: (HasCallStack, Eq a, Show a, Monoid st) => assertParse :: (HasCallStack, Eq a, Show a, Default st) =>
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> Assertion StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> Assertion
assertParse parser input = do assertParse parser input = do
ep <- runParserT (evalStateT (parser <* eof) mempty) "" input ep <- runParserT (evalStateT (parser <* eof) def) "" input
either (assertFailure.(++"\n").("\nparse error at "++).customErrorBundlePretty) either (assertFailure.(++"\n").("\nparse error at "++).customErrorBundlePretty)
(const $ return ()) (const $ return ())
ep ep
-- | Assert a parser produces an expected value. -- | Assert a parser produces an expected value.
assertParseEq :: (HasCallStack, Eq a, Show a, Monoid st) => assertParseEq :: (HasCallStack, Eq a, Show a, Default st) =>
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> Assertion StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> Assertion
assertParseEq parser input expected = assertParseEqOn parser input id expected assertParseEq parser input expected = assertParseEqOn parser input id expected
-- | Like assertParseEq, but transform the parse result with the given function -- | Like assertParseEq, but transform the parse result with the given function
-- before comparing it. -- before comparing it.
assertParseEqOn :: (HasCallStack, Eq b, Show b, Monoid st) => assertParseEqOn :: (HasCallStack, Eq b, Show b, Default st) =>
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> Assertion StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> Assertion
assertParseEqOn parser input f expected = do assertParseEqOn parser input f expected = do
ep <- runParserT (evalStateT (parser <* eof) mempty) "" input ep <- runParserT (evalStateT (parser <* eof) def) "" input
either (assertFailure . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty) either (assertFailure . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty)
(assertEqual "" expected . f) (assertEqual "" expected . f)
ep ep
-- | Assert that this stateful parser runnable in IO fails to parse -- | Assert that this stateful parser runnable in IO fails to parse
-- the given input text, with a parse error containing the given string. -- the given input text, with a parse error containing the given string.
assertParseError :: (HasCallStack, Eq a, Show a, Monoid st) => assertParseError :: (HasCallStack, Eq a, Show a, Default st) =>
StateT st (ParsecT CustomErr T.Text IO) a -> String -> String -> Assertion StateT st (ParsecT CustomErr T.Text IO) a -> String -> String -> Assertion
assertParseError parser input errstr = do assertParseError parser input errstr = do
ep <- runParserT (evalStateT parser mempty) "" (T.pack input) ep <- runParserT (evalStateT parser def) "" (T.pack input)
case ep of case ep of
Right v -> assertFailure $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n" Right v -> assertFailure $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"
Left e -> do Left e -> do
@ -113,28 +114,28 @@ assertParseError parser input errstr = do
-- | Run a stateful parser in IO like assertParse, then assert that the -- | Run a stateful parser in IO like assertParse, then assert that the
-- final state (the wrapped state, not megaparsec's internal state), -- final state (the wrapped state, not megaparsec's internal state),
-- transformed by the given function, matches the given expected value. -- transformed by the given function, matches the given expected value.
assertParseStateOn :: (HasCallStack, Eq b, Show b, Monoid st) => assertParseStateOn :: (HasCallStack, Eq b, Show b, Default st) =>
StateT st (ParsecT CustomErr T.Text IO) a StateT st (ParsecT CustomErr T.Text IO) a
-> T.Text -> T.Text
-> (st -> b) -> (st -> b)
-> b -> b
-> Assertion -> Assertion
assertParseStateOn parser input f expected = do assertParseStateOn parser input f expected = do
es <- runParserT (execStateT (parser <* eof) mempty) "" input es <- runParserT (execStateT (parser <* eof) def) "" input
case es of case es of
Left err -> assertFailure $ (++"\n") $ ("\nparse error at "++) $ customErrorBundlePretty err Left err -> assertFailure $ (++"\n") $ ("\nparse error at "++) $ customErrorBundlePretty err
Right s -> assertEqual "" expected $ f s Right s -> assertEqual "" expected $ f s
-- | These "E" variants of the above are suitable for hledger's ErroringJournalParser parsers. -- | These "E" variants of the above are suitable for hledger's ErroringJournalParser parsers.
assertParseE assertParseE
:: (HasCallStack, Eq a, Show a, Monoid st) :: (HasCallStack, Eq a, Show a, Default st)
=> StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
-> T.Text -> T.Text
-> Assertion -> Assertion
assertParseE parser input = do assertParseE parser input = do
let filepath = "" let filepath = ""
eep <- runExceptT $ eep <- runExceptT $
runParserT (evalStateT (parser <* eof) mempty) filepath input runParserT (evalStateT (parser <* eof) def) filepath input
case eep of case eep of
Left finalErr -> Left finalErr ->
let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
@ -145,7 +146,7 @@ assertParseE parser input = do
ep ep
assertParseEqE assertParseEqE
:: (Monoid st, Eq a, Show a, HasCallStack) :: (Default st, Eq a, Show a, HasCallStack)
=> StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
-> T.Text -> T.Text
-> a -> a
@ -153,7 +154,7 @@ assertParseEqE
assertParseEqE parser input expected = assertParseEqOnE parser input id expected assertParseEqE parser input expected = assertParseEqOnE parser input id expected
assertParseEqOnE assertParseEqOnE
:: (HasCallStack, Eq b, Show b, Monoid st) :: (HasCallStack, Eq b, Show b, Default st)
=> StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
-> T.Text -> T.Text
-> (a -> b) -> (a -> b)
@ -161,7 +162,7 @@ assertParseEqOnE
-> Assertion -> Assertion
assertParseEqOnE parser input f expected = do assertParseEqOnE parser input f expected = do
let filepath = "" let filepath = ""
eep <- runExceptT $ runParserT (evalStateT (parser <* eof) mempty) filepath input eep <- runExceptT $ runParserT (evalStateT (parser <* eof) def) filepath input
case eep of case eep of
Left finalErr -> Left finalErr ->
let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
@ -172,14 +173,14 @@ assertParseEqOnE parser input f expected = do
ep ep
assertParseErrorE assertParseErrorE
:: (Monoid st, Eq a, Show a, HasCallStack) :: (Default st, Eq a, Show a, HasCallStack)
=> StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
-> T.Text -> T.Text
-> String -> String
-> Assertion -> Assertion
assertParseErrorE parser input errstr = do assertParseErrorE parser input errstr = do
let filepath = "" let filepath = ""
eep <- runExceptT $ runParserT (evalStateT parser mempty) filepath input eep <- runExceptT $ runParserT (evalStateT parser def) filepath input
case eep of case eep of
Left finalErr -> do Left finalErr -> do
let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr

View File

@ -166,7 +166,7 @@ validatePostings acctRes amtRes = let
foldl (\s a -> s <> parseErrorTextPretty a) "" . foldl (\s a -> s <> parseErrorTextPretty a) "" .
bundleErrors) bundleErrors)
checkAccount = errorToFormMsg . runParser (accountnamep <* eof) "" . T.strip checkAccount = errorToFormMsg . runParser (accountnamep <* eof) "" . T.strip
checkAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) mempty) "" . T.strip checkAmount = errorToFormMsg . runParser (evalStateT (amountp <* eof) nulljournal) "" . T.strip
-- Add errors to forms with zero or one rows if the form is not a FormMissing -- Add errors to forms with zero or one rows if the form is not a FormMissing
result :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)] result :: [(Text, Text, Either (Maybe Text, Maybe Text) Posting)]