prednote-0.26.0.0: lib/Prednote/Core.hs
{-# 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