datalog-0.2.0.1: src/Database/Datalog/Rules.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
-- | FIXME: Change the adornment/query building process such that
-- conditional clauses are always processed last. This is necessary
-- so that all variables are bound.
--
-- FIXME: Add an assertion to say that ConditionalClauses cannot have
-- Free variables.
module Database.Datalog.Rules (
Adornment(..),
Term(..),
Clause(..),
AdornedClause(..),
Rule(..),
Literal(..),
Query(..),
QueryBuilder,
PartialTuple(..),
(|-),
assertRule,
relationPredicateFromName,
inferencePredicate,
ruleRelations,
issueQuery,
runQuery,
queryToPartialTuple,
queryPredicate,
lit,
negLit,
cond1,
cond2,
cond3,
cond4,
cond5,
bindQuery,
partitionRules
) where
import qualified Control.Monad.Catch as E
import Control.Monad.Trans.Class
import Control.Monad.Trans.State.Strict
import Data.Function ( on )
import Data.Hashable
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HM
import Data.List ( intercalate, groupBy, sortBy )
import Data.Maybe ( mapMaybe )
import Data.Monoid
import Data.Text ( Text )
import qualified Data.Text as T
import Text.Printf ( printf )
import Database.Datalog.Adornment
import Database.Datalog.Relation
import Database.Datalog.Errors
import Database.Datalog.Database
-- import Debug.Trace
-- debug = flip trace
data QueryState a = QueryState { intensionalDatabase :: Database a
, conditionalIdSource :: Int
, queryRules :: [(Clause a, [Literal Clause a])]
}
-- | The Monad in which queries are constructed and rules are declared
type QueryBuilder m a = StateT (QueryState a) m
nextConditionalId :: (E.MonadThrow m) => QueryBuilder m a Int
nextConditionalId = do
s <- get
let cid = conditionalIdSource s
put s { conditionalIdSource = cid + 1 }
return cid
data Term a = LogicVar !Text
-- ^ A basic logic variable. Equality is based on the
-- variable name.
| BindVar !Text
-- ^ A special variable available in queries that can be
-- bound at query execution time
| Anything
-- ^ A term that is allowed to take any value (this is
-- sugar for a fresh logic variable)
| Atom a
-- ^ A user-provided literal from the domain a
| FreshVar !Int
-- ^ A fresh logic variable, generated internally for
-- each Anything occurrence. Not exposed to the user
instance (Show a) => Show (Term a) where
show (LogicVar t) = T.unpack t
show (BindVar t) = "??" ++ T.unpack t
show (Atom a) = show a
show Anything = "*"
show (FreshVar _) = "*"
instance (Hashable a) => Hashable (Term a) where
hashWithSalt s (LogicVar t) =
s `hashWithSalt` t `hashWithSalt` (1 :: Int)
hashWithSalt s (BindVar t) =
s `hashWithSalt` t `hashWithSalt` (2 :: Int)
hashWithSalt s (Atom a) = s `hashWithSalt` a
hashWithSalt s Anything = s `hashWithSalt` (99 :: Int)
hashWithSalt s (FreshVar i) =
s `hashWithSalt` i `hashWithSalt` (22 :: Int)
instance (Eq a) => Eq (Term a) where
(LogicVar t1) == (LogicVar t2) = t1 == t2
(BindVar t1) == (BindVar t2) = t1 == t2
(Atom a1) == (Atom a2) = a1 == a2
Anything == Anything = True
FreshVar i1 == FreshVar i2 = i1 == i2
_ == _ = False
data Clause a = Clause { clauseRelation :: Relation
, clauseTerms :: [Term a]
}
instance (Eq a) => Eq (Clause a) where
(Clause r1 ts1) == (Clause r2 ts2) = r1 == r2 && ts1 == ts2
instance (Show a) => Show (Clause a) where
show (Clause p ts) =
printf "%s(%s)" (show p) (intercalate ", " (map show ts))
data AdornedClause a = AdornedClause { adornedClauseRelation :: Relation
, adornedClauseTerms :: [(Term a, Adornment)]
}
instance (Eq a) => Eq (AdornedClause a) where
(AdornedClause r1 cs1) == (AdornedClause r2 cs2) = r1 == r2 && cs1 == cs2
instance (Hashable a) => Hashable (AdornedClause a) where
hashWithSalt s (AdornedClause r ts) =
s `hashWithSalt` r `hashWithSalt` ts
instance (Show a) => Show (AdornedClause a) where
show (AdornedClause p ats) =
printf "%s(%s)" (show p) (intercalate ", " (map showAT ats))
where
showAT (t, a) = printf "%s[%s]" (show t) (show a)
-- | Body clauses can be normal clauses, negated clauses, or
-- conditionals. Conditionals are arbitrary-arity (via a list)
-- functions over literals and logic variables.
data Literal ctype a = Literal (ctype a)
| NegatedLiteral (ctype a)
| ConditionalClause Int ([a] -> Bool) [Term a] (HashMap (Term a) Int)
-- | This equality instance is complicated because conditional clauses
-- contain functions. We assign a unique id at conditional clause
-- creation time so they have identity and we can compare on that.
-- Rules from different queries cannot be compared safely, but that
-- shouldn't be a problem because there isn't really a way to sneak a
-- rule reference out of a query. If there is a shady way to do so,
-- don't because it will be bad.
instance (Eq a, Eq (ctype a)) => Eq (Literal ctype a) where
(Literal c1) == (Literal c2) = c1 == c2
(NegatedLiteral c1) == (NegatedLiteral c2) = c1 == c2
(ConditionalClause cid1 _ _ _) == (ConditionalClause cid2 _ _ _) = cid1 == cid2
_ == _ = False
instance (Hashable a, Hashable (ctype a)) => Hashable (Literal ctype a) where
hashWithSalt s (Literal c) =
s `hashWithSalt` c `hashWithSalt` (1 :: Int)
hashWithSalt s (NegatedLiteral c) =
s `hashWithSalt` c `hashWithSalt` (2 :: Int)
hashWithSalt s (ConditionalClause cid _ ts vm) =
s `hashWithSalt` cid `hashWithSalt` ts `hashWithSalt` HM.size vm
lit :: (E.MonadThrow m) => Relation -> [Term a] -> QueryBuilder m a (Literal Clause a)
lit p ts = return $ Literal $ Clause p ts
negLit :: (E.MonadThrow m) => Relation -> [Term a] -> QueryBuilder m a (Literal Clause a)
negLit p ts = return $ NegatedLiteral $ Clause p ts
cond1 :: (E.MonadThrow m, Eq a, Hashable a)
=> (a -> Bool)
-> Term a
-> QueryBuilder m a (Literal Clause a)
cond1 p t = do
cid <- nextConditionalId
return $ ConditionalClause cid (\[x] -> p x) [t] mempty
cond2 :: (E.MonadThrow m, Eq a, Hashable a)
=> (a -> a -> Bool)
-> (Term a, Term a)
-> QueryBuilder m a (Literal Clause a)
cond2 p (t1, t2) = do
cid <- nextConditionalId
return $ ConditionalClause cid (\[x1, x2] -> p x1 x2) [t1, t2] mempty
cond3 :: (E.MonadThrow m, Eq a, Hashable a)
=> (a -> a -> a -> Bool)
-> (Term a, Term a, Term a)
-> QueryBuilder m a (Literal Clause a)
cond3 p (t1, t2, t3) = do
cid <- nextConditionalId
return $ ConditionalClause cid (\[x1, x2, x3] -> p x1 x2 x3) [t1, t2, t3] mempty
cond4 :: (E.MonadThrow m, Eq a, Hashable a)
=> (a -> a -> a -> a -> Bool)
-> (Term a, Term a, Term a, Term a)
-> QueryBuilder m a (Literal Clause a)
cond4 p (t1, t2, t3, t4) = do
cid <- nextConditionalId
return $ ConditionalClause cid (\[x1, x2, x3, x4] -> p x1 x2 x3 x4) [t1, t2, t3, t4] mempty
cond5 :: (E.MonadThrow m, Eq a, Hashable a)
=> (a -> a -> a -> a -> a -> Bool)
-> (Term a, Term a, Term a, Term a, Term a)
-> QueryBuilder m a (Literal Clause a)
cond5 p (t1, t2, t3, t4, t5) = do
cid <- nextConditionalId
return $ ConditionalClause cid (\[x1, x2, x3, x4, x5] -> p x1 x2 x3 x4 x5) [t1, t2, t3, t4, t5] mempty
instance (Show a, Show (ctype a)) => Show (Literal ctype a) where
show (Literal c) = show c
show (NegatedLiteral c) = '~' : show c
show (ConditionalClause _ _ ts _) = printf "f(%s)" (show ts)
-- | A rule has a head and body clauses. Body clauses can be normal
-- clauses, negated clauses, or conditionals.
data Rule a = Rule { ruleHead :: AdornedClause a
, ruleBody :: [Literal AdornedClause a]
, ruleVariableMap :: HashMap (Term a) Int
}
instance (Show a) => Show (Rule a) where
show (Rule h b _) = printf "%s |- %s" (show h) (intercalate ", " (map show b))
instance (Eq a) => Eq (Rule a) where
(Rule h1 b1 vms1) == (Rule h2 b2 vms2) =
h1 == h2 && b1 == b2 && vms1 == vms2
instance (Hashable a) => Hashable (Rule a) where
hashWithSalt s (Rule h b vms) =
s `hashWithSalt` h `hashWithSalt` b `hashWithSalt` HM.size vms
newtype Query a = Query { unQuery :: Clause a }
deriving (Show)
infixr 0 |-
-- | Assert a rule
--
-- FIXME: Check to make sure that clause arities match their declared
-- schema.
(|-), assertRule :: (E.MonadThrow m)
=> (Relation, [Term a]) -- ^ The rule head
-> [QueryBuilder m a (Literal Clause a)] -- ^ Body literals
-> QueryBuilder m a ()
(|-) = assertRule
assertRule (p, ts) b = do
-- FIXME: Assert that Anything does not appear in the head terms
-- (that is a range restriction violation). Also check the range
-- restriction here.
b' <- sequence b
let h = Clause p ts
b'' = fst $ foldr freshenVars ([], [0..]) b'
s <- get
put s { queryRules = (h, b'') : queryRules s }
-- | Replace all instances of Anything with a FreshVar with a unique
-- (to the rule) index. This lets later evaluation stages ignore
-- these and just deal with clean FreshVars.
freshenVars :: Literal Clause a
-> ([Literal Clause a], [Int])
-> ([Literal Clause a], [Int])
freshenVars l (cs, ixSrc) =
case l of
ConditionalClause _ _ _ _ -> (l : cs, ixSrc)
Literal (Clause h ts) ->
let (ts', ixRest) = foldr freshen ([], ixSrc) ts
in (Literal (Clause h ts') : cs, ixRest)
NegatedLiteral (Clause h ts) ->
let (ts', ixRest) = foldr freshen ([], ixSrc) ts
in (NegatedLiteral (Clause h ts') : cs, ixRest)
where
freshen t (ts, src) =
case t of
Anything -> (FreshVar (head src) : ts, tail src)
_ -> (t : ts, src)
-- FIXME: Unify these and require inferred relations to be declared in
-- the schema at db construction time. That also gives an opportunity
-- to name the columns
-- | Retrieve a Relation handle from the IDB. If the Relation does
-- not exist, an error will be raised.
relationPredicateFromName :: (E.MonadThrow m)
=> Text
-> QueryBuilder m a Relation
relationPredicateFromName name = do
let rel = Relation name
idb <- gets intensionalDatabase
case rel `elem` databaseRelations idb of
False -> lift $ E.throwM (NoRelationError rel)
True -> return rel
-- | Create a new predicate that will be referenced by an EDB rule
inferencePredicate :: (E.MonadThrow m)
=> Text
-> QueryBuilder m a Relation
inferencePredicate = return . Relation
-- | A partial tuple records the atoms in a tuple (with their indices
-- in the tuple). These are primarily used in database queries.
newtype PartialTuple a = PartialTuple [Maybe a]
instance (Show a) => Show (PartialTuple a) where
show (PartialTuple vs) = show $ map show vs
-- | Convert a 'Query' into a 'PartialTuple' that can be used in a
-- 'select' of the IDB
queryToPartialTuple :: Query a -> PartialTuple a
queryToPartialTuple (Query c) =
PartialTuple $! map takeAtom ts
where
ts = clauseTerms c
takeAtom t =
case t of
Atom a -> Just a
_ -> Nothing
literalClauseRelation :: Literal AdornedClause a -> Maybe Relation
literalClauseRelation bc =
case bc of
Literal c -> Just $ adornedClauseRelation c
NegatedLiteral c -> Just $ adornedClauseRelation c
_ -> Nothing
ruleRelations :: Rule a -> [Relation]
ruleRelations (Rule h bcs _) = adornedClauseRelation h : mapMaybe literalClauseRelation bcs
-- | Turn a Clause into a Query. This is meant to be the last
-- statement in a QueryBuilder monad.
issueQuery :: (E.MonadThrow m) => Relation -> [Term a] -> QueryBuilder m a (Query a)
issueQuery r ts = return $ Query $ Clause r ts
-- | Run the QueryBuilder action to build a query and initial rule
-- database
--
-- Rules are adorned (marking each variable as Free or Bound as they
-- appear) before being returned.
runQuery :: (E.MonadThrow m, Eq a, Hashable a)
=> QueryBuilder m a (Query a) -> Database a -> m (Query a, [(Clause a, [Literal Clause a])])
runQuery qm idb = do
(q, QueryState _ _ rs) <- runStateT qm (QueryState idb 0 [])
return (q, rs)
-- | Group rules by their head relations. This is needed to perform
-- semi-naive evaluation easily.
partitionRules :: [Rule a] -> [[Rule a]]
partitionRules = groupBy gcmp . sortBy scmp
where
scmp = compare `on` (adornedClauseRelation . ruleHead)
gcmp = (==) `on` (adornedClauseRelation . ruleHead)
queryPredicate :: Query a -> Relation
queryPredicate = clauseRelation . unQuery
-- | Apply bindings to a query
bindQuery :: Query a -> [(Text, a)] -> Query a
bindQuery (Query (Clause r ts)) bs =
Query $ Clause r $ foldr applyBinding [] ts
where
applyBinding t acc =
case t of
LogicVar _ -> t : acc
BindVar name ->
case lookup name bs of
Nothing -> error ("No binding provided for BindVar " ++ show name)
Just b -> Atom b : acc
Anything -> t : acc
Atom _ -> t : acc
FreshVar _ -> error "Users cannot provide FreshVars"