Monadic plumbing

Table of Contents

Here we tie up the loose ends of our first-order logic theorem prover by putting all the rules together.

1. The list monad

As a quick reminder, here is the definition of the list monad.

instance Monad [] where
  return a     = [a]
  [] >>= _     = []
  (a:as) >>= k = k a ++ as >>= k

It'll be useful to have this definition on hand as we manipulate lists and the functions that act on them!

2. Promoting rules

Recall the type we associate with a rule (remember that a rule is a way of extending a tableau at one of its terminal nodes, according to the main connective of the formula that occurs at that node and the formula's sign):

type Rule = Path -> SignedForm -> [Branch]

where

type SigneForm = (Form, Bool)
type Path = [SignedForm]
type Branch = [SignedForm]

Thus given some path (which the rule might use to determine, e.g., which names are fresh), a rule takes a signed formula at some node onto the set of new branches that should extend out from that node once the rule has applied.

What we would like is a way of promoting a Rule to a function which can be applied to an entire Path. That is, we require a function

applyRule :: Rule -> Path -> [Path]

which applies a rule to each path in some tableau—by targeting all the nodes on that path—in order to give back a new tableau. Then, using applyRule, we can write a function

allRules :: Path -> [Path]
allRules p = do p0 <- applyRule (const andRule) p
                p1 <- applyRule (const orRule) p0
                p2 <- applyRule (const notRule) p1
                p3 <- applyRule gammaRule p2
                p4 <- applyRule deltaRule p3
                return p4

which starts with some path p, and then successively applies each rule to that path. That is, first andRule is applied to p via applyRule—note that const :: a -> b -> a simply allows andRule to ignore the first (Path type) argument—and then p1 is drawn from the result; next, orRule is applied to p1, and p2 is drawn from the result; etc., until we've applied each rule exactly once.

Given such a function, we can implement a way of looping all the rules over a tableau and then checking each path for a contradiction.

For the latter, we need the function

contradiction :: [Path] -> Bool
contradiction [] = False
contradiction ((f, b) : fs) = (f, not b) `elem` fs || contradiction fs

That is, for each signed formula on some path, we check whether the same formula, but with opposite sign, occurs somewhere else on the path; if not, we check to see if the rest of the path (excluding that signed formula) is somehow contradictory.

Now let's write a function that implements one loop! This function, loop, should have type

loop :: Tableau -> Tableau

where

type Tableau = [Maybe Path]

In other words, a Tableau is a list of things which might be paths. If some path has been determined to be contradictory, it should get replaced by a Nothing, indicating that we never have to worry about it again. If not, we should apply allRules to it and map Just over the resulting paths, indicating that they are still live. Thus we may define loop as

loop tableau = do
  maybefs <- tableau
  case maybefs of
    Nothing -> pure Nothing
    Just fs -> do
      newFs <- allRules fs
      if contradictory newFs
      then pure Nothing
      else pure (Just newFs)

We are almost there! Now, given a list of signed formulae, we would like to be able to check if they are contradictory, given some depth n of application of our rules. Thus we write a function

depth :: Int -> [SignedForm] -> Bool
depth n sfs = all (== Nothing) (iterate loop [map Just sfs] !! n)

which turns the list of signed formulae sfs into a starting Tableau (by doing [map Just sfs]) and then loops the rules over it n times, using iterate. If the tableau is contradictory after n loops—that is, if all the branches are Nothing—we assume we have a contradiction; otherwise, we we assume the signed formulae are not contradictory. Note that we might sometimes be wrong.1

In terms of our function depth, we my define a function that checks (at some depth) whether or not some set of formulae entails another formula. We do this by applying depth to a starting tableau in which the possibly-entailed formula is paired with False and the other formulae are paired with True. Thus if the formula is truly entailed, a contradiction will ensue.

entails :: Int -> [Form] -> Form -> Bool
entails n antecedents consequent = depth n sfs
  where sfs = (consequent, False) : map (, True) antecedents

Yay!

2.1. applyRule

Yeah, yeah, but we haven't even defined applyRule yet. Jeez, what is wrong with us? That's okay, we can define it now.

applyRule :: Rule -> Path -> [Path]
applyRule rule p = applyRule' rule p [[]]
  where applyRule' :: Rule -> Path -> [Path] -> [Path]
        applyRule _ [] t = t
        applyRule rule (f:fs) t = applyRule fs t'
          where t' = do p <- t
                        b <- rule (f:fs ++ p) f
                        return (p ++ b)

In words, to apply a rule to an entire path, we apply the rule to one node (f) of the path and then concatenate the resulting branches to the paths that result from applying the rule to the rest of the nodes (fs) of the path.

Footnotes:

1

In general, first-order logic is only semi-decideable. Thus if some set of formulae is contradictory, we may in principle find the contradiction by running a theorem prover at some depth, but this depth is upper bounded by infinity. Indeed, we could try to loop forever, but then our algorithm would never terminate on non-contradictory sets of formulae.

Author: Julian Grove

Created: 2023-11-13 Mon 16:14

Validate