Prolog in Haskell - Unification-related predicates
In the previous log, we have reached a basic naive Prolog implementation, but Prolog comes with predicates changing/impacting the unification:
fail/0which make unification fail!/0, called cut, which freeze backtracking, all other clauses won't be evaluated
They are often used together, it's called cut-fail, for example, we can prevent salty-fruits salad as follows:
fruit(tomato).
fruit(melon).
sweet(melon).
salty(tomato).
fruits_salad(X) :- salty(X), !, fail.
fruits_salad(X) :- fruit(X), sweet(x).
We cannot implement these predicates directly as they have a different unification mechanism, we have to redefine Predicate to carry it, as follows:
newtype Predicate = Predicate (Substitution -> [Predicate] -> Goal -> Logic Substitution)
Names and arguments, if any, will be checked by the Predicate, which simplifies query as follows:
query ss ps g = do
Predicate f <- lift ps
f ss ps g
We have to move the rest of the logic in predicate, as follows:
predicate name firstClause otherClauses =
Predicate $ \ss ps (Goal gFunctor gArgs) -> do
guard (gFunctor == Atom name)
freshnessIndex <- get
put (freshnessIndex + 1)
(rawArgs, rawBody) <- lift $ firstClause : otherClauses
let cArgs = rename freshnessIndex rawArgs
body = map (renameGoal freshnessIndex) rawBody
solveGoals ss' =
\case
[] -> lift [ss']
(g : gs) -> do
ss'' <- query ss' ps g
solveGoals ss'' gs
case eqTypeRep (typeOf gArgs) (typeOf cArgs) of
Just HRefl -> do
ssa <- lift $ maybeToList $ unify ss cArgs gArgs
solveGoals (ss <> ssa) body
Nothing -> lift []
The smart constructor here helps us to only have a local change, our tests are still passing!
Defining fail is as trivial as follows:
failP =
Predicate $ \_ _ (Goal gFunctor _) -> do
guard (gFunctor == Atom "fail")
empty
However !, cut, is a bit more involved, we have to track the current level of unification and the last "cut-level", it should be part of our state we can plainly define as follows:
type Logic = StateT State []
data State = State
{ freshness :: Int,
}
initialState =
State
{ freshness = 0,
depth = 0,
cutDepth = -1
}
nextFreshessIndex = do
s <- get
put s {freshness = s.freshness + 1}
pure s.freshness
Then, for each query, we have to:
- Check the depth against
cutDepth(skip otherwise), increment depth, go through each clause, decrement depth as follows:
query ss ps g = do
s <- get
guard $ s.cutDepth < s.depth
put s {depth = s.depth + 1}
Predicate f <- lift ps
ns <- f ss ps g
s' <- get
put s' {depth = s'.depth - 1}
pure ns
Then, we can easily define cut as follows:
cut =
Predicate $ \ss _ (Goal gFunctor _) -> do
guard (gFunctor == Atom "!")
s <- get
put s {cutDepth = s.depth}
pure ss
We can add the following tests:
spec =
describe "Prolog" $ do
describe "hasGoal" $ do
it "rule fact with cut-fail the arg should be valid" $
hasGoal [failP, cut, fruit, salty, fruitSalad2] ("fruitSalad" @ atom "melon") `shouldBe` True
it "rule fact with cut-fail the arg should be invalid" $
hasGoal [failP, cut, fruit, salty, fruitSalad2] ("fruitSalad" @ atom "tomato") `shouldBe` False
describe "runQuery" $ do
it "rules facts, based on cut-fail and atom-based goal should be valid" $
runQuery [fruit, failP, cut, fruitSalad2] ("fruitSalad" @ var "F") `shouldBe` [Map.singleton (Var "F") (atom "melon")]
-- * Fixtures
fruit =
predicate "fruit" (atom "tomato", []) [(atom "melon", [])]
salad =
predicate "salad" (atom "melon", []) []
fruitSalad =
predicate "fruitSalad" (var "X", ["fruit" @ var "X", "salad" @ var "X"]) []
eq =
predicate "eq" ((var "X", var "X"), []) []
salty =
predicate "salty" (atom "tomato", []) []
fruitSalad2 =
predicate "fruitSalad" (var "X", ["salty" @ var "X", "!" @ (), "fail" @ ()]) [(var "X", ["fruit" @ var "X"])]
This is mostly it for our implementation, next time we will see "higher order predicates".