{-# LANGUAGE OverloadedStrings #-} -- | Trees of predicates. -- -- Exports names which conflict with Prelude names, so you probably -- want to import this module qualified. module Data.Prednote.Pdct ( -- * The Pdct tree Label , Hide , Pdct(..) , Node(..) -- * Creating Pdct. -- | All functions create Pdct that are shown by default. , operand , and , or , not , (&&&) , (|||) , always , never -- * Controlling whether Pdct are shown in the results , hide , show , hideTrue , hideFalse -- * Renaming Pdct , rename -- * Result , Result(..) , RNode(..) -- * Showing and evaluating Pdct , evaluate , evaluateNode , IndentAmt , Level , ShowAll , showResult , showTopResult , showPdct , filter , verboseFilter -- * Helpers for building common Pdct -- ** Non-overloaded , compareBy , compareByMaybe , greaterBy , lessBy , equalBy , greaterEqBy , lessEqBy , notEqBy -- ** Overloaded , compare , greater , less , equal , greaterEq , lessEq , notEq , parseComparer ) where -- # Imports import Data.Functor.Contravariant import Data.Text (Text) import qualified Data.Text as X import Data.Monoid ((<>), mconcat, mempty) import Data.String (fromString) import qualified System.Console.Rainbow as R import Prelude hiding (not, and, or, compare, filter, show) import qualified Prelude -- # Pdct type type Label = Text -- | Determines whether a result is shown by default. type Hide = Bool -- | A predicate. Each Pdct contains a tree of Node. data Pdct a = Pdct { pLabel :: Label -- ^ Label used when showing the results , pHide :: (Bool -> Hide) -- ^ As results are computed, this function is applied to the -- result. If this function returns False, then this Pdct will not -- be shown by default in the results. , pNode :: Node a } data Node a = And [Pdct a] -- ^ Conjunction. If any Pdct in the list is False, the result is -- False. If the list is empty, the result is True. | Or [Pdct a] -- ^ Disjunction. If at least one Pdct in the list is True, the -- result it True. If the list is empty, the result is False. | Not (Pdct a) -- ^ Negation | Operand (a -> Bool) -- ^ Most basic building block. -- | Renames the top level of the Pdct. The function you pass will be -- applied to the old name. rename :: (Text -> Text) -> Pdct a -> Pdct a rename f p = p { pLabel = f (pLabel p) } -- | Always True always :: Pdct a always = Pdct "always True" (const False) (Operand (const True)) -- | Always False never :: Pdct a never = Pdct "always False" (const False) (Operand (const False)) -- | Creates and labels operands. operand :: Label -> (a -> Bool) -> Pdct a operand l = Pdct l (const False) . Operand -- | Creates And Pdct using a generic name and :: [Pdct a] -> Pdct a and = Pdct "and" (const False) . And -- | Creates Or Pdct using a generic name or :: [Pdct a] -> Pdct a or = Pdct "or" (const False) . Or -- | Creates Not Pdct using a generic name not :: Pdct a -> Pdct a not = Pdct "not" (const False) . Not -- | Changes a Pdct so it is always hidden by default. hide :: Pdct a -> Pdct a hide p = p { pHide = const True } -- | Changes a Pdct so it is always shown by default. show :: Pdct a -> Pdct a show p = p { pHide = const False } -- | Changes a Pdct so that it is hidden if its result is True. hideTrue :: Pdct a -> Pdct a hideTrue p = p { pHide = id } -- | Changes a Pdct so that it is hidden if its result is False. hideFalse :: Pdct a -> Pdct a hideFalse p = p { pHide = Prelude.not } -- | Forms a Pdct using 'and'; assigns a generic label. (&&&) :: Pdct a -> Pdct a -> Pdct a (&&&) x y = Pdct "and" (const False) (And [x, y]) infixr 3 &&& -- | Forms a Pdct using 'or'; assigns a generic label. (|||) :: Pdct a -> Pdct a -> Pdct a (|||) x y = Pdct "or" (const False) (Or [x, y]) infixr 2 ||| instance Contravariant Pdct where contramap f (Pdct l d n) = Pdct l d $ contramap f n instance Contravariant Node where contramap f n = case n of And ls -> And $ map (contramap f) ls Or ls -> Or $ map (contramap f) ls Not o -> Not $ contramap f o Operand g -> Operand $ \b -> g (f b) -- # Result -- | The result from evaluating a Pdct. data Result = Result { rLabel :: Label -- ^ The label from the original Pdct , rBool :: Bool -- ^ The boolean result from evaluating the node. If the node is an -- operand, this is the result of applying the operand function to -- the subject. Otherwise, this is the result of application of the -- appropriate boolean operation to the child nodes. , rHide :: Hide -- ^ Is this result hidden in the result by default? Hiding only -- affects presentation; it does not affect how this Pdct affects -- any parent Pdct. , rNode :: RNode } deriving (Eq, Show) data RNode = RAnd [Result] | ROr [Result] | RNot Result | ROperand Bool deriving (Eq, Show) -- | Applies a Pdct to a particular value, known as the subject. evaluate :: a -> Pdct a -> Result evaluate a (Pdct l d n) = Result l r d' rn where rn = evaluateNode a n r = case rn of RAnd ls -> all rBool ls ROr ls -> any rBool ls RNot x -> Prelude.not . rBool $ x ROperand b -> b d' = d r evaluateNode :: a -> Node a -> RNode evaluateNode a n = case n of And ls -> RAnd (map (evaluate a) ls) Or ls -> ROr (map (evaluate a) ls) Not l -> RNot (evaluate a l) Operand f -> ROperand (f a) -- # Types and functions for showing -- | The number of spaces to use for each level of indentation. type IndentAmt = Int -- | How many levels of indentation to use. Typically you will start -- this at zero. It is incremented by one for each level as functions -- descend through the tree. type Level = Int -- | Indents text, and adds a newline to the end. indent :: IndentAmt -> Level -> [R.Chunk] -> [R.Chunk] indent amt lvl cs = idt : (cs ++ [nl]) where idt = fromString (replicate (lvl * amt) ' ') nl = fromString "\n" -- # Showing Pdct -- | Creates a plain Chunk from a Text. plain :: Text -> R.Chunk plain = R.Chunk mempty -- | Shows a Pdct tree without evaluating it. showPdct :: IndentAmt -> Level -> Pdct a -> [R.Chunk] showPdct amt lvl (Pdct l _ pd) = case pd of And ls -> indent amt lvl [plain ("and - " <> l)] <> mconcat (map (showPdct amt (lvl + 1)) ls) Or ls -> indent amt lvl [plain ("or - " <> l)] <> mconcat (map (showPdct amt (lvl + 1)) ls) Not t -> indent amt lvl [plain ("not - " <> l)] <> showPdct amt (lvl + 1) t Operand _ -> indent amt lvl [plain ("operand - " <> l)] instance Show (Pdct a) where show = X.unpack . X.concat . map R.text . showPdct 2 0 filter :: Pdct a -> [a] -> [a] filter pd as = map fst . Prelude.filter (rBool . snd) . zip as . map (flip evaluate pd) $ as -- # Showing Result labelBool :: Text -> Bool -> [R.Chunk] labelBool t b = [open, trueFalse, close, blank, txt] where trueFalse = if b then "TRUE" <> R.f_green else "FALSE" <> R.f_red open = "[" close = "]" blank = plain (X.replicate blankLen " ") blankLen = X.length "discard" - X.length (R.text trueFalse) + 1 txt = plain t type ShowAll = Bool -- | Shows a Result in a pretty way with colors and indentation. showResult :: IndentAmt -- ^ Indent each level by this many spaces -> ShowAll -- ^ If True, shows all Pdct, even ones where 'rHide' is -- True. Otherwise, respects 'rHide' and does not show hidden Pdct. -> Level -- ^ How deep in the tree we are; this increments by one for each -- level of descent. -> Result -- ^ The result to show -> [R.Chunk] showResult amt sa lvl (Result lbl rslt hd nd) | hd && Prelude.not sa = [] | otherwise = firstLine ++ restLines where firstLine = indent amt lvl $ labelBool lbl rslt restLines = case nd of RAnd ls -> f False ls ROr ls -> f True ls RNot r -> showResult amt sa (lvl + 1) r ROperand _ -> [] f stopOn ls = concatMap sr ls' ++ end where ls' = takeThrough ((== stopOn) . rBool) ls sr = showResult amt sa (lvl + 1) end = if ls' `shorter` ls then indent amt (lvl + 1) ["(short circuit)"] else [] -- | @shorter x y@ is True if list x is shorter than list y. Lazier -- than taking the length of each list and comparing the results. shorter :: [a] -> [a] -> Bool shorter [] [] = False shorter (_:_) [] = False shorter [] (_:_) = True shorter (_:xs) (_:ys) = shorter xs ys -- | For instance, -- > takeThrough odd [2,4,6,7,8] == [2,4,6,7] takeThrough :: (a -> Bool) -> [a] -> [a] takeThrough _ [] = [] takeThrough f (x:xs) = x : if f x then [] else takeThrough f xs -- | Shows the top of a Result tree and all the child Results. Adds a -- short label at the top of the tree. showTopResult :: X.Text -- ^ Label to add to the top of the tree. -> IndentAmt -- ^ Indent each level by this many spaces -> Level -- ^ Indent the top by this many levels -> ShowAll -- ^ If True, shows all Pdct, even ones where 'rHide' is -- True. Otherwise, respects 'rHide' and does not show hidden Pdct. -> Result -- ^ The result to show -> [R.Chunk] showTopResult txt i lvl sd r = showResult i sd lvl r' where r' = r { rLabel = rLabel r <> " - " <> txt } -- | Filters a list. Also returns chunks describing the process. verboseFilter :: (a -> X.Text) -- ^ How to describe each subject -> IndentAmt -- ^ Indent each level by this many spaces -> ShowAll -- ^ If True, shows all Pdct, even ones where 'rHide' is -- True. Otherwise, respects 'rHide' and does not show hidden Pdct. -> Pdct a -- ^ Used to perform the filtering -> [a] -> ([R.Chunk], [a]) verboseFilter desc amt sa pd as = (chks, as') where rs = map (flip evaluate pd) as subjAndRslts = zip as rs mkChks (subj, rslt) = showTopResult (desc subj) amt 0 sa rslt chks = concatMap mkChks subjAndRslts as' = map fst . Prelude.filter (rBool . snd) $ subjAndRslts -- # Comparisons -- | Build a Pdct that compares items. compareBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare an item against the right hand side. Return LT -- if the item is less than the right hand side; GT if greater; EQ -- if equal to the right hand side. -> Ordering -- ^ When subjects are compared, this ordering must be the result in -- order for the Pdct to be True; otherwise it is False. The subject -- will be on the left hand side. -> Pdct a compareBy itemDesc typeDesc cmp ord = Pdct l (const False) (Operand f) where l = typeDesc <> " is " <> cmpDesc <> " " <> itemDesc cmpDesc = case ord of LT -> "less than" GT -> "greater than" EQ -> "equal to" f subj = cmp subj == ord -- | Overloaded version of 'compareBy'. compare :: (Show a, Ord a) => Text -- ^ Description of the type of thing being matched -> a -- ^ The right hand side of the comparison. -> Ordering -- ^ When subjects are compared, this ordering must be the result in -- order for the Pdct to be True; otherwise it is False. The subject -- will be on the left hand side. -> Pdct a compare typeDesc a ord = compareBy itemDesc typeDesc cmp ord where itemDesc = X.pack . Prelude.show $ a cmp item = Prelude.compare item a -- | Builds a Pdct for items that might fail to return a comparison. compareByMaybe :: Text -- ^ How to show the item being compared -> Text -- ^ Description of type of thing being matched -> (a -> Maybe Ordering) -- ^ How to compare against right hand side. If Nothing, a Pdct that -- always returns False is returned. -> Ordering -- ^ Ordering that must result for the Pdct to be True -> Pdct a compareByMaybe itemDesc typeDesc cmp ord = Pdct l (const False) (Operand f) where l = typeDesc <> " is " <> cmpDesc <> " " <> itemDesc cmpDesc = case ord of LT -> "less than" GT -> "greater than" EQ -> "equal to" f subj = case cmp subj of Nothing -> False Just ord' -> ord == ord' greater :: (Show a, Ord a) => Text -- ^ How to show the item being compared; used to describe the Pdct -> a -- ^ The right hand side of the comparison. -> Pdct a greater d a = compare d a GT less :: (Show a, Ord a) => Text -- ^ How to show the item being compared; used to describe the Pdct -> a -- ^ The right hand side of the comparison. -> Pdct a less d a = compare d a LT equal :: (Show a, Ord a) => Text -- ^ How to show the item being compared; used to describe the Pdct -> a -- ^ The right hand side of the comparison. -> Pdct a equal d a = compare d a EQ greaterEq :: (Show a, Ord a) => Text -- ^ How to show the item being compared; used to describe the Pdct -> a -- ^ The right hand side of the comparison. -> Pdct a greaterEq d a = greater d a ||| equal d a lessEq :: (Show a, Ord a) => Text -- ^ How to show the item being compared; used to describe the Pdct -> a -- ^ The right hand side of the comparison. -> Pdct a lessEq d a = less d a ||| equal d a notEq :: (Show a, Ord a) => Text -- ^ How to show the item being compared; used to describe the Pdct -> a -- ^ The right hand side of the comparison. -> Pdct a notEq d a = not $ equal d a greaterBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare two items -> Pdct a greaterBy iD tD cmp = compareBy iD tD cmp GT lessBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare two items -> Pdct a lessBy iD tD cmp = compareBy iD tD cmp LT equalBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare two items -> Pdct a equalBy iD tD cmp = compareBy iD tD cmp EQ greaterEqBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare two items -> Pdct a greaterEqBy iD tD cmp = greaterBy iD tD cmp ||| equalBy iD tD cmp lessEqBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare two items -> Pdct a lessEqBy iD tD cmp = lessBy iD tD cmp ||| equalBy iD tD cmp notEqBy :: Text -- ^ How to show the item being compared; used to describe the Pdct -> Text -- ^ Description of the type of thing that is being matched -> (a -> Ordering) -- ^ How to compare two items -> Pdct a notEqBy iD tD cmp = not $ equalBy iD tD cmp -- | Parses a string to find the correct comparer; returns the correct -- function to build a Pdct. parseComparer :: Text -- ^ The string with the comparer to be parsed -> (Ordering -> Pdct a) -- ^ A function that, when given an ordering, returns a Pdct -> Maybe (Pdct a) -- ^ If an invalid comparer string is given, Nothing; otherwise, the -- Pdct. parseComparer t f | t == ">" = Just (f GT) | t == "<" = Just (f LT) | t == "=" = Just (f EQ) | t == "==" = Just (f EQ) | t == ">=" = Just (f GT ||| f EQ) | t == "<=" = Just (f LT ||| f EQ) | t == "/=" = Just (not $ f EQ) | t == "!=" = Just (not $ f EQ) | otherwise = Nothing