{-# LANGUAGE BangPatterns #-} -- | 'Pred' core functions. If your needs are simple, "Prednote.Prebuilt" -- is easier to use. However, the types and functions in this module -- give you more control. -- -- Each function in this module that returns a 'Pred' returns one with -- the following characteristics: -- -- * No 'static' name -- -- Upon evaluation: -- -- * 'visible' is always 'shown' -- -- * 'short' is either 'Nothing' or @'Just' ('const' [])@ -- -- * 'dynamic' is always @'const' []@ -- -- Thus, the 'Pred' created by this module are rather bare-bones, but -- you can modify them as you see fit; "Prednote.Prebuilt" already -- does this for you. -- -- This module exports some names that conflict with Prelude names, so -- you might want to do something like -- -- > import qualified Prednote.Pred.Core as P module Prednote.Core where import Rainbow import Prelude hiding (filter, not) import qualified Prelude import Data.Functor.Contravariant (Contravariant(..)) import Data.Tree import qualified Data.Text as X import Data.Maybe -- | Indicates how to display text. This function is applied to an -- 'Int' that is the level of indentation; each level of descent -- through a tree of 'Pred' increments this 'Int' by one. Because the -- function returns a list of 'Chunk', you can use multiple colors. -- Typically this function will indent text accordingly, with a -- newline at the end. type Chunker = Int -> [Chunk] -- | A rose tree of predicates. data Pred a = Pred { static :: Tree Chunker -- ^ A tree of static names, allowing you to identify the 'Pred' -- without applying it to a subject. , evaluate :: a -> Tree Output -- ^ Evaluates a 'Pred' by applying it to a subject. } instance Contravariant Pred where contramap f (Pred s e) = Pred s (e . f) -- | The result of evaluating a 'Pred'. data Output = Output { result :: Bool , visible :: Visible -- ^ Results that are not 'Visible' are not shown by the 'report' -- function. , short :: Maybe Chunker -- ^ Indicates whether there was a short circuit when evaluating -- this 'Pred'. A short circuit occurs when the 'Pred' does not -- need to evaluate all of its children in order to reach a -- result. If 'Nothing', there was no short circuit; otherwise, -- this is a 'Just' with a 'Chunker' providing a way to display -- the short circuit. , dynamic :: Chunker -- ^ The dynamic label; this indicates how 'report' will show the -- 'Pred' to the user after it has been evaluated. } instance Show Output where show (Output r v _ _) = "output - result: " ++ show r ++ " visible: " ++ (show . unVisible $ v) -- | Is this result visible? If not, 'Prednote.report' will not show it. newtype Visible = Visible { unVisible :: Bool } deriving (Eq, Ord, Show) -- | Shown by 'Prednote.report' shown :: Visible shown = Visible True -- | Hidden by 'Prednote.report' hidden :: Visible hidden = Visible False -- | No 'Pred' in the list may be 'False' for 'all' to be 'True'. An -- empty list of 'Pred' yields a 'Pred' that always returns 'True'. -- May short circuit. all :: [Pred a] -> Pred a all ls = Pred st' ev where st' = Node (const []) . map static $ ls ev a = go [] ls where go soFar [] = Node (Output True shown Nothing (const [])) (reverse soFar) go soFar (x:xs) = let tree = evaluate x a r = result . rootLabel $ tree shrt = case xs of [] -> Nothing _ -> Just (const []) out = Output r shown shrt (const []) cs = reverse (tree:soFar) in case xs of [] -> Node out cs _ | Prelude.not r -> Node out cs | otherwise -> go cs xs -- | At least one 'Pred' in the list must be 'True' for the resulting -- 'Pred' to be 'True'. An empty list of 'Pred' yields a 'Pred' that -- always returns 'False'. May short circuit. any :: [Pred a] -> Pred a any ls = Pred st' ev where st' = Node (const []) . map static $ ls ev a = go [] ls where go soFar [] = Node (Output False shown Nothing (const [])) (reverse soFar) go soFar (x:xs) = let tree = evaluate x a r = result . rootLabel $ tree shrt = case xs of [] -> Nothing _ -> Just (const []) out = Output r shown shrt (const []) cs = reverse (tree:soFar) in case xs of [] -> Node out cs _ | r -> Node out cs | otherwise -> go cs xs -- | Negates the child 'Pred'. Never short circuits. not :: Pred a -> Pred a not pd = Pred st' ev where st' = Node (const []) [static pd] ev a = Node nd [c] where nd = Output res shown Nothing (const []) (res, c) = (Prelude.not r, t) where t = evaluate pd a r = result . rootLabel $ t -- | Fanout. May short circuit. fan :: ([Bool] -> (Bool, Visible, Maybe Int)) -- ^ This function is applied to a list of the 'result' from -- evaluating the child 'Pred' on each fanout item. The function -- must return a triple, with the 'Bool' indicating success or -- failure, 'Visible' for visibility, and 'Maybe' 'Int' to indicate -- whether a short circuit occurred; this must be 'Nothing' if there -- was no short circuit, or 'Just' with an 'Int' to indicate a short -- circuit, with the 'Int' indicating that a short circuit occurred -- after examining the given number of elements. -- -- The resulting 'Pred' always short circuits if the previous -- function returns a 'Just' 'Int' with the 'Int' being less than -- zero. Otherwise, the resulting 'Pred' short circuits if -- the 'Int' is less than the number of elements returned by the -- fanout function. -> (a -> [b]) -- ^ Fanout function -> Pred b -> Pred a fan get fn pd = Pred st' ev where st' = Node (const []) [static pd] ev a = Node nd cs where nd = Output r v shrt (const []) (r, v, mayInt) = get bools shrt = case mayInt of Nothing -> Nothing Just s | s < 0 -> Just (const []) | cs `shorter` allcs -> Just (const []) | otherwise -> Nothing bs = fn a allcs = map (evaluate pd) bs bools = map (result . rootLabel) allcs cs = case mayInt of Nothing -> allcs Just i -> take i allcs -- | Fanout all. The resulting 'Pred' is 'True' if no child item -- returns 'False'; an empty list of child items returns 'True'. May -- short circuit. fanAll :: (a -> [b]) -- ^ Fanout function -> Pred b -> Pred a fanAll = fan get where get = go 0 where go !c ls = case ls of [] -> (True, shown, Just c) x:xs | Prelude.not x -> (False, shown, Just (c + 1)) | otherwise -> go (c + 1) xs -- | Fanout any. The resulting 'Pred' is 'True' if at least one child -- item returns 'True'; an empty list of child items returns 'False'. -- May short circuit. fanAny :: (a -> [b]) -- ^ Fanout function -> Pred b -> Pred a fanAny = fan get where get = go 0 where go !c ls = case ls of [] -> (False, shown, Just c) x:xs | x -> (True, shown, Just (c + 1)) | otherwise -> go (c + 1) xs -- | Fanout at least. The resulting 'Pred' is 'True' if at least the -- given number of child items return 'True'. May short circuit. fanAtLeast :: Int -- ^ Find at least this many. If this number is less than or equal -- to zero, 'fanAtLeast' will always return 'True'. -> (a -> [b]) -- ^ Fanout function -> Pred b -> Pred a fanAtLeast i = fan get where get = go 0 0 where go !found !c ls | found >= i = (True, shown, Just c) | otherwise = case ls of [] -> (False, shown, Just c) x:xs -> go fnd' (c + 1) xs where fnd' | x = found + 1 | otherwise = found -- | Indents and formats output for display. report :: Int -- ^ Start at this level of indentation. -> Tree Output -> [Chunk] report l (Node n cs) | (== hidden) . visible $ n = [] | otherwise = this ++ concatMap (report (l + 1)) cs ++ shrt where this = dynamic n l shrt = maybe [] ($ (l + 1)) . short $ n -- | Indents and formats static labels for display. This is a 'plan' -- for how the 'Pred' would be applied. plan :: Int -- ^ Start at this level of indentation. -> Pred a -> [Chunk] plan lvl pd = go lvl (static pd) where go l (Node n cs) = this ++ concatMap (go (l + 1)) cs where this = n l instance Show (Pred a) where show = X.unpack . X.concat . concat . map text . plan 0 -- | Applies a 'Pred' to a single subject and returns the 'result'. test :: Pred a -> a -> Bool test p = result . rootLabel . evaluate p -- | Like 'test' but also returns the accompanying 'report'. testV :: Pred a -> a -> (Bool, [Chunk]) testV p a = (result . rootLabel $ t, report 0 t) where t = evaluate p a -- | Like 'Prelude.filter'. filter :: Pred a -> [a] -> [a] filter p = Prelude.filter (test p) -- | Like 'filter' but also returns a list of 'report', with one -- 'report' for each list item. filterV :: Pred a -> [a] -> ([a], [Chunk]) filterV p as = (mapMaybe fltr (zip as rslts), cks) where fltr (a, r) | result . rootLabel $ r = Just a | otherwise = Nothing rslts = map (evaluate p) as cks = concatMap (report 0) rslts -- | @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