lattest-lib-0.1.1: src/Lattest/Model/BoundedMonad.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE ConstraintKinds #-}
{- |
A /bounded monad/ is a type constructor which represents the observable perspective on the state of an automaton, also called a
/state configuration/. A system will internally have a state, but this state can not always be observed or inferred for an external viewer. To
model the difference between internal and observable state, we define automata as having a type of internal state, say q, and
an observable state configuration over q.
In this module, we define two such state configurations:
* deterministic state configurations, where every behaviour leads to a single state,
* distributive lattices, or positive boolean formulas, where the observable behaviour is expressed as a logical expression over states.
Here, 'observed behaviour' is formally a trace or sequence of observable actions. Thus, after a trace, the system is
in a state configuration over states. The state configuration is computed by taking transitions, following the monadic interpret
of the state configuration. If the automaton does not exhibit a given trace of actions, then the state configuration after
that trace is either 'forbidden' or 'underspecified'. Here, 'forbidden' expresses that the automaton does not allow the
given trace, whereas 'underspecified' expresses that the automaton does not specify the trace, hence the trace is allowed.
The name 'bounded monad' is based on bounded lattices, which have a top and bottom element. For more details on the use of top and bottom in
state configurations, see
* [/Ramon Janssen/, Refinement and partiality for model-based testing (Doctoral dissertation), 2022, Chapter 4](https://repository.ubn.ru.nl/bitstream/handle/2066/285020/285020.pdf)
-}
module Lattest.Model.BoundedMonad (
-- * State configurations
-- ** Deterministic
Det(..),
-- ** Distributive lattice in CNF
FreeLattice(FreeLattice),
atom,
top,
bot,
-- * Specifiednesss
Specifiedness(..),
BoundedConfiguration,
specifiedness,
forbidden,
underspecified,
isForbidden,
isUnderspecified,
-- ** Auxiliary Specifiedness functions
isAllowed,
isSpecified,
isIndefinite,
isConclusive,
-- ** Utility types
BoundedMonad,
BoundedFunctor,
-- ** General non-determinism
JoinSemiLattice,
MeetSemiLattice,
(\/),
(/\),
-- ** Mapping between lattices and boolean expressions
BooleanConfiguration,
asExpr,
asDualExpr,
-- ** 'Data.OrdMonad' re-export, for convenience.
module OM,
joins,
meets,
disjunction,
(\$/),
conjunction,
(/$\)
)
where
import qualified Lattest.Model.Symbolic.Expr as E
import qualified Data.List as List
import qualified Data.Set as Set
import Data.OrdMonad as OM
-- | Deterministic state configuration. This means that an automaton is either in a single state, or in an explicit forbidden configuration, or in an explicit underspecified configuration.
data Det q = Det q | ForbiddenDet | UnderspecDet deriving (Ord, Eq)
instance BoundedConfiguration Det where
isForbidden ForbiddenDet = True
isForbidden _ = False
isUnderspecified UnderspecDet = True
isUnderspecified _ = False
forbidden = ForbiddenDet
underspecified = UnderspecDet
instance Functor Det where
fmap f (Det s) = Det (f s)
fmap _ UnderspecDet = UnderspecDet
fmap _ ForbiddenDet = ForbiddenDet
instance Applicative Det where
Det f <*> (Det s) = Det (f s)
ForbiddenDet <*> _ = ForbiddenDet
UnderspecDet <*> _ = UnderspecDet
_ <*> ForbiddenDet = ForbiddenDet
_ <*> UnderspecDet = UnderspecDet
pure = Det
instance Monad Det where
Det s >>= f = f s
ForbiddenDet >>= _ = ForbiddenDet
UnderspecDet >>= _ = UnderspecDet
instance Foldable Det where
foldr f q (Det q') = f q' q
foldr _ q _ = q
instance Show a => Show (Det a) where
show (Det a) = show a
show ForbiddenDet = "-forbidden-"
show UnderspecDet = "-underspecified-"
{-|
Free distributive lattice, or a positive boolean formula, in CNF-format.
-}
newtype FreeLattice a = FreeLattice (Set.Set (Set.Set a)) deriving (Eq, Ord, Foldable)
-- | A single state embedded in a free distributive lattice.
atom :: a -> FreeLattice a
atom = ordReturn
-- | The free distributive lattice element ⊥, or false.
bot :: FreeLattice a
bot = forbidden
-- | The free distributive lattice element ⊤, or true.
top :: FreeLattice a
top = underspecified
-- | Combine a collection of FreeLattices using '(/\)'
meets :: (Foldable f, Ord a) => f (FreeLattice a) -> FreeLattice a
meets = foldr (/\) top
-- | Combine a collection of FreeLattices using '(\/)'
joins :: (Foldable f, Ord a) => f (FreeLattice a) -> FreeLattice a
joins = foldr (\/) bot
-- | Combine a collection of states using '(/\)'
conjunction :: (Functor f, Foldable f, Ord a) => f a -> FreeLattice a
conjunction = meets . fmap atom
-- | Synonym for 'conjunction'
(/$\) :: (Functor f, Foldable f, Ord a) => f a -> FreeLattice a
(/$\) = conjunction
-- | Combine a collection of states using '(\/)'
disjunction :: (Functor f, Foldable f, Ord a) => f a -> FreeLattice a
disjunction = joins . fmap atom
-- | Synonym for 'disjunction'
(\$/) :: (Functor f, Foldable f, Ord a) => f a -> FreeLattice a
(\$/) = disjunction
instance BoundedConfiguration FreeLattice where
isForbidden (FreeLattice x) = any Set.null x
isUnderspecified (FreeLattice x) = Set.null x
forbidden = FreeLattice $ Set.singleton Set.empty
underspecified = FreeLattice Set.empty
instance OM.OrdMonad FreeLattice where
ordBind (FreeLattice x) f = FreeLattice $ cnfJoin $ Set.map (Set.map f1) x
where
f1 y = let FreeLattice z = f y in z
ordReturn x = FreeLattice $ Set.singleton $ Set.singleton x
cnfJoin :: (Ord a) => Set.Set (Set.Set (Set.Set (Set.Set a))) -> Set.Set (Set.Set a)
cnfJoin = reduceAll . Set.map Set.unions . Set.unions . Set.map nAryCartesianProduct
where
-- some possible optimizations: 1) don't compare every element to and 2) use the ordering on sets to avoid half of the comparisons, 3) ensure that this ordering is such that absorbing/neutral elements are compared first and avoid more work in that case
reduceAll sets = Set.filter (not . isProperSupersetOfAny sets) sets
nAryCartesianProduct :: (Ord a) => Set.Set (Set.Set a) -> Set.Set (Set.Set a)
nAryCartesianProduct j = Set.map Set.fromList $ Set.fromList $ sequence $ Set.toList $ Set.map Set.toList j
isProperSupersetOfAny :: Ord a => Set.Set (Set.Set a) -> Set.Set a -> Bool
isProperSupersetOfAny sets a = any (isProperSupersetOf a) (Set.toList sets)
where
isProperSupersetOf :: Ord a => Set.Set a -> Set.Set a -> Bool
isProperSupersetOf set potentialSubset = (potentialSubset `Set.isSubsetOf` set) && not (set `Set.isSubsetOf` potentialSubset)
instance OM.OrdFunctor FreeLattice where
ordMap f (FreeLattice x) = FreeLattice $ Set.map (Set.map f) x
instance OM.OrdTraversable FreeLattice where
ordTraverse f (FreeLattice x) = FreeLattice <$> ordTraverse (ordTraverse f) x
ordSequenceA (FreeLattice x) = FreeLattice <$> ordTraverse ordSequenceA x
instance Ord a => JoinSemiLattice (FreeLattice a) where
(FreeLattice x) \/ (FreeLattice y) = FreeLattice $ Set.map Set.unions $ nAryCartesianProduct $ Set.fromList [x,y]
instance Ord a => MeetSemiLattice (FreeLattice a) where
(FreeLattice x) /\ (FreeLattice y) =
let x' = Set.filter (not . isProperSupersetOfAny y) x
y' = Set.filter (not . isProperSupersetOfAny x) y
in FreeLattice (x' `Set.union` y')
instance Show a => Show (FreeLattice a) where
show l
| isForbidden l = "⊥"
| isUnderspecified l = "⊤"
show (FreeLattice x) = case Set.toList x of
[conjunct] -> List.intercalate " ∨ " $ show <$> Set.toList conjunct
conjuncts -> List.intercalate " ∧ " $ showDisjunct <$> conjuncts
where
showDisjunct :: Show a => Set.Set a -> String
showDisjunct y = case Set.toList y of
[e] -> show e
disjuncts -> "(" ++ List.intercalate " ∨ " (show <$> disjuncts) ++ ")"
{-|
Specifiednesss describe wether behaviour (a sequence of actions) is allowed a stateful specification model. 'Forbidden' describes that
behaviour is not allowed, not is any subsequent behaviour. 'Underspecified' describes that behaviour is allowed and any subsequent
behaviour is also allowed. 'Indefinite' means that the behaviour is allowed, and subsequent behaviour may be forbidden, underspecified,
or may again be 'Indefinite'.
-}
data Specifiedness = Underspecified | Forbidden | Indefinite deriving (Eq, Ord, Show, Read)
{-|
Specifiedness configurations are state configurations which have a representation for forbidden and underspecified configurations.
-}
class BoundedConfiguration m where
forbidden :: m t -- ^ The forbidden state configuration.
underspecified :: m t -- ^ The underspecified state configuration.
isForbidden :: m t -> Bool -- ^ Is this state configuration forbidden?
isUnderspecified :: m t -> Bool -- ^ Is this state configuration underspecified?
-- | Extract the current 'Specifiedness' from a bounded monad.
specifiedness :: BoundedConfiguration m => m t -> Specifiedness
specifiedness c
| isForbidden c = Forbidden
| isUnderspecified c = Underspecified
| otherwise = Indefinite
-- | Is the configuration a representation of the 'Indefinite' specifiedness?
isIndefinite :: (BoundedConfiguration m) => m t -> Bool
isIndefinite p = specifiedness p == Indefinite
-- | Is the configuration a representation of "definitive", i.e., 'Forbidden' or 'Underspecified'?
isConclusive :: (BoundedConfiguration m) => m t -> Bool
isConclusive p = specifiedness p /= Indefinite
-- | Is the configuration a representation of "allowed", i.e., 'Indefinite' or 'Underspecified'?
isAllowed :: (BoundedConfiguration m) => m t -> Bool
isAllowed = not . isForbidden
-- | Is the configuration a representation of "specified", i.e., 'Indefinite' or 'Forbidden'?
isSpecified :: (BoundedConfiguration m) => m t -> Bool
isSpecified = not . isUnderspecified
-- | Abbreviation for types which are both bounded configurations and Monads.
type BoundedMonad m = (BoundedConfiguration m, OM.OrdMonad m)
-- | Abbreviation for types which are both bounded configurations and Functors.
type BoundedFunctor m = (BoundedConfiguration m, OM.OrdFunctor m)
-- | Semi-lattices with a binary join ('or') operation
class JoinSemiLattice a where
(\/) :: a -> a -> a
-- | Semi-lattices with a binary meet ('and') operation
class MeetSemiLattice a where
(/\) :: a -> a -> a
--this would be very sensible but it confuses the compiler greatly. Maybe the UndecidableInstances and Overlapping language extensions don't like each other?
--instance Lattice a => JoinSemiLattice a where
-- join = (L.\/)
class BooleanConfiguration m where -- TODO: possibly this class can be less ad-hoc, e.g. via some lattice-theoretic concept
asExpr :: m (E.Expr Bool) -> E.Expr Bool
instance BooleanConfiguration Det where
asExpr (Det q) = q
asExpr ForbiddenDet = E.sFalse
asExpr UnderspecDet = E.sTrue
instance BooleanConfiguration FreeLattice where
asExpr (FreeLattice x) = Set.foldr (E..&&) E.sTrue $ Set.map (Set.foldr (E..||) E.sFalse) x
asDualExpr :: (OrdFunctor m, BooleanConfiguration m) => m (E.Expr Bool) -> E.Expr Bool
asDualExpr m = E.sNot $ asExpr $ E.sNot OM.<#> m
instance Traversable Det where
sequenceA (Det a) = Det <$> a
sequenceA ForbiddenDet = pure ForbiddenDet
sequenceA UnderspecDet = pure UnderspecDet