Lecture 25

Propositional Logic: Theorem Provers

A Tautology Checker

Our next program will be a simple brute-force tautology checker. In years gone by, this took a fair amount of effort, but our infrastructure work makes this fairly straightforward.

Our proof checker will work as follows. Given a proposition p, it will:

  1. Compute the set v of variables that occur in p.
  2. Generate a list valuations consisting of all possible maps from v to Bool.
  3. Evaluate the proposition p at each valuation in turn, producing a list of valuations at which the proposition is false
  4. Generate output, either noting that the formula is a tautology, or providing a refuting refutation.

The first step is particularly simple, given that we've made Prop an instance of FoldMap.

variables :: (Eq a) => Prop a -> [a] variables = nub . toList

The toList function comes from Data.Foldable, and gives the list of a's that occur in the Var's of a Prop a. We use nub to reduce this list to have one occurrence of each a.

Generating the list of valuations is trickier.

valuations :: Ord a => Prop a -> [Map a Bool] valuations = map Map.fromList . sequence . assignments . variables where assignments = map (\v -> [(v,True),(v,False)])

Let's take this piece by piece, with an example proposition:

testProp :: Prop String testProp = getProp $ read "a -> a | b"

First, we extract the variables:

> variables testProp ["a","b"]

Next, we compute the assignments of truth values to each variable:

> assignments . variables $ testProp [[("a",True),("a",False)],[("b",True),("b",False)]]

Next up, we apply sequence, from Control.Monad. This is the great magic here. Recall:

sequence :: Monad m => [m a] -> m [a]

Intuitive, sequence does a bind on each of the monads on the list, returning the list of results. In this case, we're applying sequence to a list of lists, so the monad in question is [], and what happens is that we'll build a list, each of whose elements is a list, consisting of one element from each of the original list's element lists. Stated more succinctly, applied to a list of lists, sequence returns the cartesian product of the constituent lists.

> sequence . assignments . variables $ testProp [[("a",True),("b",True)],[("a",True),("b",False)],[("a",False),("b",True)],[("a",False),("b",False)]]

Finally, we apply map Map.fromList to this list, obtaining the list of valuations.

Next up, we have to evaluate the proposition at each valuation. It is tempting to write a simple evaluator, which would look like this:

eval :: Ord a => Map a Bool -> Prop a -> Bool eval valf = primrec (valf !) id not (&&) (||) implies where implies x y = not x || y

Isn't it nice how much work primrec saves? But there is a danger here: the call to Map.! will throw an exception if it is applied to a map and a key where the key isn't in the map. It is the programmer's responsibility to make sure that this doesn't happen, either by a careful analysis of the program's logic, or by pre-flighting the call to Map.! with a call to Map.member. In this case, we can be confident that the calls to Map.! are safe, because the maps are valuations based on a list of all the variables that occur in the proposition.

What we can't be confident of, though, is that we won't at some later date decide to use eval in another program, and forget about the possibility that it will throw an exception if passed a valuation that's undefined on one of the variables that occur in the Prop. Documentation is a partial cure, but there is a more Haskellish approach. What we'll do is rewrite the code so that it does the evaluation in a monad, and the documentation that encourages the use of a monad that can handle errors. We'll note to our future selves that if we use Map.lookup to access our valuation, it returns a result in the Maybe monad, which is adequate for our immediate purposes. But this enables more sophisticated approaches too, e.g., returning a result in Either a Bool, with Left a indicating a failed attempt to lookup a. At first glance, monadifying eval seems to add a lot of complication. This turns out not to be the case:

eval :: Monad m => (a -> m Bool) -> Prop a -> m Bool eval valf = primrec valf return (liftM not) (liftM2 (&&)) (liftM2 (||)) (liftM2 implies) where implies x y = not x || y

All we've had to do is to make sure is adjust each of the case-handling functions to return a monadic result, and in the cases where recursive calls are made, to expect monadic arguments. And we can do that by lifting each function that we used originally. It is worth a quick review of the type constraints on m, and how they've changed. The initial version of eval used a Map a Bool to represent a valuation, and the constraint Ord a comes along with using a as a key type in a Map. The monadic version expected a valuation to be represented as a function that returned a Bool within a monad. So the specific dependency on Map was lost (presumably to arise in our caller), but now we need the constraint that m is a monad because the liftM and liftM2 functions impose it.

Next, we generate a list of exceptional valuations, i.e., valuations for which a given proposition is not true:

exceptions :: Ord a => Prop a -> [Map a Bool] exceptions = [v | v <- valuations p, eval (`Map.lookup` v) p /= Just True]

Finally, there's the IO code to make it all work. Following the example of norm from last time, taut will process its command line arguments, reading each as a Proposition, and then reporting whether the resulting Proposition is or is not a tautology. There's nothing especially remarkable about the code involved:

processArg :: String -> IO () processArg arg = do mp <- try (readIO arg) :: IO (Either IOError Proposition) case mp of Left _ -> putStrLn $ "Could not parse \'" ++ arg ++ "\'." Right p -> case exceptions . getProp $ p of [] -> putStrLn $ show p ++ " is a tautology." v:_ -> do putStrLn $ show p ++ " is not a tautology, cf." putStr . unlines . map (\(k,v) -> " " ++ k ++ ": " ++ show v) . Map.toList $ v main :: IO () main = do args <- getArgs mapM_ processArg args

A sample run looks like this:

$ taut "a -> a" "a -> b" "(a -> b) -> (b -> c) -> (a -> c)" "a ->" a -> a is a tautology. a -> b is not a tautology, cf. a: True b: False (a -> b) -> (b -> c) -> a -> c is a tautology. Could not parse 'a ->'. $

On one hand, this is a fairly useful little program, but on another, it's a bit unsatisfying. If we process a refutable proposition, we're given a refuting valuation. But if the proposition is a tautology, all we get is a bare assertion that this is so. If we want to do more, we'll have to work harder.

Code