Prolog in Haskell - Refining unification
In the previous log, we have started a naive Prolog implementation, we have introduced unification, but it has the followings shortcomings:
- Variables are mixed-up, when there are rules-recursions, same-named variable will go in the same set
Substitutionare accumulated via(<>), which indistinctly- Produce
Substitutioncontains intermediaryVar
To highlight this, let's draft a test, as follows:
it "rules facts and atom-based goal should be valid" $
query mempty [fruit, salad, fruitSalad] ("fruitSalad" @ var "F") `shouldBe` [Map.singleton (Var "F") (atom "melon")]
Sadly, the result is quite different, see the following:
expected: [fromList [("F",TAtom "melon")]]
but got: [fromList [("F",TAtom "tomato"),("X",TVar "F")],fromList [("F",TAtom "melon"),("X",TVar "F")]]
Propagate not used variables break the function.
The first step is to extract the variables from arguments as follows:
-- ...
-- ...
vars _ = mempty
-- ...
vars =
\case
TAtom _ -> mempty
TVar v -> Set.singleton v
TGoal (Goal _ args) -> vars args
-- ...
vars (x, y) = vars x <> vars y
-- ...
vars (x, y, z) = vars x <> vars y <> vars z
The next step is to filter substitutions as follows:
runQuery ps g@(Goal _ gArgs) = flip Map.restrictKeys targetVars <$> query mempty ps g
where targetVars = vars gArgs
It slightly improve the following results:
expected: [fromList [("F",TAtom "melon")]]
but got: [fromList [("F",TAtom "tomato")],fromList [("F",TAtom "melon")]]
The problem is that, in the current substitution application phase, variables are somehow turned global, creating false positives.
To do so, the process of evaluating a predicate should make each variable globally unique, it's often referred as freshness, starting as follows at Args-level:
-- ...
-- ...
rename _ () = ()
-- ...
rename i = \case
TVar (Var v) -> TVar (Var (v <> "_" <> show i))
TAtom a -> TAtom a
TGoal g -> TGoal (renameGoal i g)
renameGoal i (Goal f a) = Goal f (rename i a)
-- ...
rename i (x, y) = (rename i x, rename i y)
-- ...
rename i (x, y, z) = (rename i x, rename i y, rename i z)
We suffix all variables with a unique number.
We should also replace recursively variables as follows:
unifyTerm ss x y =
-- ...
sub =
\case
TVar v | Just t <- Map.lookup v ss -> sub t
o -> o
To have a unique number, we have to introduce a dedicate state monad holding a counter as follows:
type Logic = StateT Int []
query ss ps (Goal gFunctor gArgs) = do
Predicate pFunctor pClauses <- lift ps
guard (gFunctor == pFunctor)
freshnessIndex <- get
put (freshnessIndex + 1)
(rawArgs, rawBody) <- lift $ toList pClauses
let cArgs = rename freshnessIndex rawArgs
let body = map (renameGoal freshnessIndex) rawBody
case eqTypeRep (typeOf gArgs) (typeOf cArgs) of
Just HRefl -> do
ssa <- lift $ maybeToList $ unify ss cArgs gArgs
solveGoals (ss <> ssa) body
Nothing -> lift []
where
solveGoals ss' =
\case
[] -> lift [ss']
(g : gs) -> do
ss'' <- query ss' ps g
solveGoals ss'' gs
Each time we get the freshness index, we increment it to keep it unique, which makes the following test to work:
it "rules facts and atom-based goal should be valid" $
runQuery [fruit, salad, fruitSalad] ("fruitSalad" @ var "F") `shouldBe` [Map.singleton (Var "F") (atom "melon")]
Beyond this, one of the things I mean the most, but I know the price is too high, is the ability for a variable to unify with itself, which make the following equality beautiful:
eq(X, X).
And our code is supporting it already, as shown in the following test:
it "rule fact with referencing twice the arg should be valid" $
hasGoal [eq] ("eq" @ (atom "tomato", atom "tomato")) `shouldBe` True
it "rule fact with referencing twice the arg should be invalid" $
hasGoal [eq] ("eq" @ (atom "tomato", atom "carrots")) `shouldBe` False
Sadly, querying is broken as highlighted is the following test:
it "rule fact with referencing twice the arg should be valid" $
runQuery [eq] ("eq" @ (atom "tomato", var "X")) `shouldBe` [Map.singleton (Var "X") (atom "tomato")]
Which gives the following result:
test/Spec.hs:46:57:
1) Prolog.runQuery rule fact with referencing twice the arg should be valid
expected: [fromList [("X",TAtom "tomato")]]
but got: [fromList [("X",TVar "X_0")]]
We only lack a substitution in the query processing, let's extract our helper and apply it as follows:
runQuery ps g@(Goal _ gArgs) = ground <$> evalStateT (query mempty ps g) 0
where targetVars = vars gArgs
ground ss = Map.fromSet (substitute ss . TVar) targetVars
substitute ss =
\case
TVar v | Just t <- Map.lookup v ss -> substitute ss t
o -> o
In the next log we will support built-in predicates, which change the unification process, and why is it useful.