{-# LANGUAGE OverloadedStrings #-} module Prednote.Core ( -- * Predicates and their creation PredM(..) , Pred , predicate , predicateM , contramapM -- * Predicate combinators -- ** Primitive combinators -- -- | You might consider these combinators to be \"primitive\" in the -- sense that you can build a 'Pred' for any user-defined type by -- using these combinators alone, along with 'contramap'. Use -- '&&&', '|||', and 'contramap' to analyze product types. Use 'switch' -- and 'contramap' to analyze sum types. For a simple example, see the -- source code for 'maybe', which is a simple sum type. For more -- complicated examples, see the source code for 'any' and 'all', as -- a list is a sum type where one of the summands is a (recursive!) -- product type. , (&&&) , (|||) , not , switch -- ** Convenience combinators -- -- | These were written using entirely the \"primitive\" combinators -- given above. , any , all , maybe -- * Labeling , addLabel -- * Constant predicates , true , false , same -- * Evaluating predicates , test , testM , runPred , verboseTest , verboseTestStdout -- * Results and converting them to 'Chunk's -- -- | Usually you will not need these functions and types, as the -- functions and types above should meet most use cases; however, -- these are here so the test suites can use them, and in case you -- need them. , Condition(..) , Value(..) , Label(..) , Labeled(..) , Passed(..) , Failed(..) , Result(..) , splitResult , resultToChunks , passedToChunks , failedToChunks ) where import Rainbow import Rainbow.Types (_yarn) import Data.Monoid import Data.Functor.Contravariant import Prelude hiding (all, any, maybe, and, or, not) import qualified Prelude import Data.Text (Text) import qualified Data.Text as X import Data.List (intersperse) import Data.Functor.Identity import Control.Applicative import qualified Data.ByteString as BS -- | Like 'contramap' but allows the mapping function to run in a -- monad. contramapM :: Monad m => (a -> m b) -> PredM m b -> PredM m a contramapM conv (PredM f) = PredM $ \a -> conv a >>= f -- | Describes the condition; for example, for a @'Pred' 'Int'@, -- this might be @is greater than 5@; for a @'Pred' 'String'@, this -- might be @begins with \"Hello\"@. newtype Condition = Condition [Chunk Text] deriving (Eq, Ord, Show) instance Monoid Condition where mempty = Condition [] mappend (Condition x) (Condition y) = Condition (x ++ y) -- | Stores the representation of a value. newtype Value = Value [Chunk Text] deriving (Eq, Ord, Show) instance Monoid Value where mempty = Value [] mappend (Value x) (Value y) = Value (x ++ y) -- | Gives additional information about a particular 'Pred' to aid the -- user when viewing the output. newtype Label = Label [Chunk Text] deriving (Eq, Ord, Show) instance Monoid Label where mempty = Label [] mappend (Label x) (Label y) = Label (x ++ y) -- | Any type that is accompanied by a set of labels. data Labeled a = Labeled [Label] a deriving (Eq, Ord, Show) instance Functor Labeled where fmap f (Labeled l a) = Labeled l (f a) -- | A 'Pred' that returned 'True' data Passed = PTerminal Value Condition -- ^ A 'Pred' created with 'predicate' | PAnd (Labeled Passed) (Labeled Passed) -- ^ A 'Pred' created with '&&&' | POr (Either (Labeled Passed) (Labeled Failed, Labeled Passed)) -- ^ A 'Pred' created with '|||' | PNot (Labeled Failed) -- ^ A 'Pred' created with 'not' deriving (Eq, Ord, Show) -- | A 'Pred' that returned 'False' data Failed = FTerminal Value Condition -- ^ A 'Pred' created with 'predicate' | FAnd (Either (Labeled Failed) (Labeled Passed, Labeled Failed)) -- ^ A 'Pred' created with '&&&' | FOr (Labeled Failed) (Labeled Failed) -- ^ A 'Pred' created with '|||' | FNot (Labeled Passed) -- ^ A 'Pred' created with 'not' deriving (Eq, Ord, Show) -- | The result of processing a 'Pred'. newtype Result = Result (Labeled (Either Failed Passed)) deriving (Eq, Ord, Show) -- | Returns whether this 'Result' failed or passed. splitResult :: Result -> Either (Labeled Failed) (Labeled Passed) splitResult (Result (Labeled l ei)) = case ei of Left n -> Left (Labeled l n) Right g -> Right (Labeled l g) -- | Predicates. Is an instance of 'Contravariant', which allows you -- to change the type using 'contramap'. Though the constructor is -- exported, ordinarily you shouldn't need to use it; other functions -- in this module create 'PredM' and manipulate them as needed. -- -- The @f@ type variable is an arbitrary context; ordinarily this type -- will be an instance of 'Monad', and some of the bindings in this -- module require this. That allows you to run predicate computations -- that run in some sort of context, allowing you to perform IO, -- examine state, or whatever. If you only want to do pure -- computations, just use the 'Pred' type synonym. newtype PredM f a = PredM { runPredM :: (a -> f Result) } -- | Predicates that do not run in any context. type Pred = PredM Identity -- | Runs pure 'Pred' computations. runPred :: Pred a -> a -> Result runPred (PredM f) a = runIdentity $ f a instance Show (PredM f a) where show _ = "Pred" instance Contravariant (PredM f) where contramap f (PredM g) = PredM (g . f) -- | Creates a new 'PredM' that run in some arbitrary context. In -- @predicateM cond f@, @cond@ describes the condition, while @f@ -- gives the predicate function. For example, if @f@ is @(> 5)@, then -- @cond@ might be @"is greater than 5"@. predicateM :: Functor f => (a -> f (Bool, Value, Condition)) -> PredM f a predicateM f = PredM f' where f' a = fmap mkResult $ f a where mkResult (b, val, cond) = Result (Labeled [] r) where r | b = Right (PTerminal val cond) | otherwise = Left (FTerminal val cond) -- | Creates a new 'Pred' that do not run in any context. In -- @predicate cond f@, @cond@ describes the condition, while @f@ gives -- the predicate function. For example, if @f@ is @(> 5)@, then -- @cond@ might be @"is greater than 5"@. predicate :: (a -> (Bool, Value, Condition)) -> Pred a predicate f = predicateM (fmap return f) -- | And. Returns 'True' if both argument 'Pred' return 'True'. Is -- lazy in its second argment; if the first argument returns 'False', -- the second is ignored. (&&&) :: Monad m => PredM m a -> PredM m a -> PredM m a (PredM fL) &&& r = PredM $ \a -> do resL <- fL a ei <- case splitResult resL of Left n -> return (Left (FAnd (Left n))) Right g -> do let PredM fR = r resR <- fR a return $ case splitResult resR of Left b -> Left (FAnd (Right (g, b))) Right g' -> Right (PAnd g g') return (Result (Labeled [] ei)) infixr 3 &&& -- | Or. Returns 'True' if either argument 'Pred' returns 'True'. Is -- lazy in its second argument; if the first argument returns 'True', -- the second argument is ignored. (|||) :: Monad m => PredM m a -> PredM m a -> PredM m a (PredM fL) ||| r = PredM $ \a -> do resL <- fL a ei <- case splitResult resL of Left b -> do let PredM fR = r resR <- fR a return $ case splitResult resR of Left b' -> Left $ FOr b b' Right g -> Right $ POr (Right (b, g)) Right g -> return (Right (POr (Left g))) return (Result (Labeled [] ei)) infixr 2 ||| -- | Negation. Returns 'True' if the argument 'Pred' returns 'False'. not :: Functor m => PredM m a -> PredM m a not (PredM f) = PredM $ \a -> fmap g (f a) where g a = Result (Labeled [] rslt) where rslt = case splitResult a of Left b -> Right (PNot b) Right y -> Left (FNot y) -- | Uses the appropriate 'Pred' depending on the 'Either' value. In -- @'test' ('switch' l r) e@, the resulting 'Pred' returns the result -- of @l@ if @e@ is 'Left' or the result of @r@ if @e@ is 'Right'. Is -- lazy, so the the argument 'Pred' that is not used is ignored. switch :: PredM m a -> PredM m b -> PredM m (Either a b) switch pa pb = PredM (either fa fb) where PredM fa = pa PredM fb = pb -- | Did this 'Result' pass or fail? resultToBool :: Result -> Bool resultToBool (Result (Labeled _ ei)) = either (const False) (const True) ei -- | Always returns 'True' true :: Applicative f => PredM f a true = predicateM (const (pure trip)) where trip = (True, mempty, Condition [chunk "always returns True"]) -- | Always returns 'False' false :: Applicative f => PredM f a false = predicateM (const (pure trip)) where trip = (False, mempty, Condition [chunk "always returns False"]) -- | Always returns its argument same :: Applicative f => PredM f Bool same = predicateM (\b -> pure (b, (Value [(chunk . X.pack . show $ b)]), Condition [chunk "is returned"])) -- | Adds descriptive text to a 'Pred'. Gives useful information for -- the user. The label is added to the top 'Pred' in the tree; any -- existing labels are also retained. Labels that were added last -- will be printed first. For an example of this, see the source code -- for 'any' and 'all'. addLabel :: Functor f => [Chunk Text] -> PredM f a -> PredM f a addLabel s (PredM f) = PredM f' where f' a = fmap g (f a) where g (Result (Labeled ss ei)) = Result (Labeled (Label s : ss) ei) -- | Like 'Prelude.any'; is 'True' if any of the list items are -- 'True'. An empty list returns 'False'. Is lazy; will stop -- processing if it encounters a 'True' item. any :: (Monad m, Applicative m) => PredM m a -> PredM m [a] any pa = contramap f (switch (addLabel [chunk "cons cell"] pConsCell) pEnd) where pConsCell = contramap fst (addLabel [chunk "head"] pa) ||| contramap snd (addLabel [chunk "tail"] (any pa)) f ls = case ls of [] -> Right () x:xs -> Left (x, xs) pEnd = predicateM (const (pure (False, Value [chunk "end of list"], Condition [chunk "always returns False"]))) -- | Like 'Prelude.all'; is 'True' if none of the list items is -- 'False'. An empty list returns 'True'. Is lazy; will stop -- processing if it encouters a 'False' item. all :: (Monad m, Applicative m) => PredM m a -> PredM m [a] all pa = contramap f (switch (addLabel [chunk "cons cell"] pConsCell) pEnd) where pConsCell = contramap fst (addLabel [chunk "head"] pa) &&& contramap snd (addLabel [chunk "tail"] (all pa)) f ls = case ls of x:xs -> Left (x, xs) [] -> Right () pEnd = predicateM (const (pure (True, Value [chunk "end of list"], Condition [chunk "always returns True"]))) -- | Create a 'Pred' for 'Maybe'. maybe :: Applicative m => Bool -- ^ What to return on 'Nothing' -> PredM m a -- ^ Analyzes 'Just' values -> PredM m (Maybe a) maybe onEmp pa = contramap f (switch emp (addLabel [chunk "Just value"] pa)) where emp | onEmp = predicateM (const (pure (True, noth, Condition [chunk "always returns True"]))) | otherwise = predicateM (const (pure (False, noth, Condition [chunk "always returns False"]))) noth = Value [chunk "Nothing"] f may = case may of Nothing -> Left () Just a -> Right a explainAnd :: [Chunk Text] explainAnd = [chunk "(and)"] explainOr :: [Chunk Text] explainOr = [chunk "(or)"] explainNot :: [Chunk Text] explainNot = [chunk "(not)"] -- | Runs a 'Pred' against a value. testM :: Functor f => PredM f a -> a -> f Bool testM (PredM p) = fmap (either (const False) (const True)) . fmap splitResult . p -- | Runs a 'Pred' against a value, without a context. test :: Pred a -> a -> Bool test p a = runIdentity $ testM p a -- | Runs a 'Pred' against a particular value; also returns a list of -- 'Chunk' describing the steps of evaulation. verboseTestM :: Functor f => PredM f a -> a -> f ([Chunk Text], Bool) verboseTestM (PredM f) a = fmap g (f a) where g rslt = (resultToChunks rslt, resultToBool rslt) verboseTest :: Pred a -> a -> ([Chunk Text], Bool) verboseTest p a = runIdentity $ verboseTestM p a -- | Obtain a list of 'Chunk' describing the evaluation process. resultToChunks :: Result -> [Chunk Text] resultToChunks = either (failedToChunks 0) (passedToChunks 0) . splitResult -- | A colorful label for 'True' values. lblTrue :: [Chunk Text] lblTrue = [chunk "[", chunk "TRUE" & fore green, chunk "]"] -- | A colorful label for 'False' values. lblFalse :: [Chunk Text] lblFalse = [chunk "[", chunk "FALSE" & fore red, chunk "]"] -- | Append two lists of 'Chunk', with an intervening space if both -- lists are not empty. (<+>) :: [Chunk Text] -> [Chunk Text] -> [Chunk Text] l <+> r | full l && full r = l <> [chunk " "] <> r | otherwise = l <> r where full = Prelude.any (Prelude.not . X.null) . map _yarn -- | Append two lists of 'Chunk', with an intervening hyphen if both -- lists have text. (<->) :: [Chunk Text] -> [Chunk Text] -> [Chunk Text] l <-> r | full l && full r = l <> hyphen <> r | otherwise = l <> r where full = Prelude.any (Prelude.not . X.null) . map _yarn hyphen :: [Chunk Text] hyphen = [chunk " - "] indentAmt :: Int indentAmt = 2 spaces :: Int -> [Chunk Text] spaces i = (:[]) . chunk . X.replicate (i * indentAmt) . X.singleton $ ' ' newline :: [Chunk Text] newline = [chunk "\n"] labelToChunks :: Label -> [Chunk Text] labelToChunks (Label cks) = cks explainTerminal :: Value -> Condition -> [Chunk Text] explainTerminal (Value v) (Condition c) = v ++ (chunk " " : c) -- | Obtain a list of 'Chunk' describing the evaluation process. passedToChunks :: Int -- ^ Number of levels of indentation -> Labeled Passed -> [Chunk Text] passedToChunks i (Labeled l p) = this <> rest where this = spaces i <> (lblTrue <+> (labels `sep` explain)) <> newline labels = concat . intersperse hyphen . map labelToChunks $ l nextPass = passedToChunks (succ i) nextFail = failedToChunks (succ i) (explain, rest, sep) = case p of PTerminal v c -> (explainTerminal v c, [], (<->)) PAnd p1 p2 -> (explainAnd, nextPass p1 <> nextPass p2, (<+>)) POr ei -> (explainOr, more, (<+>)) where more = case ei of Left y -> nextPass y Right (n, y) -> nextFail n <> nextPass y PNot n -> (explainNot, nextFail n, (<+>)) -- | Obtain a list of 'Chunk' describing the evaluation process. failedToChunks :: Int -- ^ Number of levels of indentation -> Labeled Failed -> [Chunk Text] failedToChunks i (Labeled l p) = this <> rest where this = spaces i <> (lblFalse <+> (labels `sep` explain)) <> newline labels = concat . intersperse hyphen . map labelToChunks $ l nextPass = passedToChunks (succ i) nextFail = failedToChunks (succ i) (explain, rest, sep) = case p of FTerminal v c -> (explainTerminal v c, [], (<->)) FAnd ei -> (explainAnd, more, (<+>)) where more = case ei of Left n -> nextFail n Right (y, n) -> nextPass y <> nextFail n FOr n1 n2 -> (explainOr, nextFail n1 <> nextFail n2, (<+>)) FNot y -> (explainNot, nextPass y, (<+>)) -- | Like 'verboseTest', but results are printed to standard output. -- Primarily for use in debugging or in a REPL. verboseTestStdout :: Pred a -> a -> IO Bool verboseTestStdout p a = do let (cks, r) = verboseTest p a mkr <- byteStringMakerFromEnvironment mapM_ BS.putStr . chunksToByteStrings mkr $ cks return r