Propositional logic and recursive datatypes
0.1. Formal grammars in Haskell
- Haskell's type system can be used to represent grammars as datatypes.
- Grammars of non-trivial formal languages/natural languages typically involve recursive rules for well-formed formulas/sentences
- An example from a CFG for natural language:
S -> NP VP VP -> V S
- "She is upset."
- "Zach said she is upset."
- "Jenna believes Zach said she is upset."
0.2. Formal grammars in Haskell cont.
- In order to implement recursive grammars in Haskell, we use recursive datatypes.
- Operations that apply to inhabitants of recursive datatypes are (typically) recursive functions.
- Today we'll learn about recursive datatypes/functions in Haskell, using the simple example of propositional logic.
0.3. Homework
- Next week we'll talk about semantics, again using propositional logic as an example.
- For homework, read chapter 5 of Computational semantics with functional programming "Formal semantics for fragments".
1. Propositional logic
1.1. Sentences of propositional logic
Sentences of propositional logic:
- \(p \wedge q\)
- \(p \wedge (q \rightarrow r)\)
- \(p \vee \neg (q \wedge p)\)
- \(\bot \rightarrow (\top \vee (q \wedge r))\)
1.2. Grammar of propositional logic
To state a grammar for propositional logic, we first need a set of variables \(\mathbf{Var} := \set{p, q, r, \ldots}\), and a set of constants \(\set{\top,\bot}\).
- If \(p \in \mathbf{Var} \cup \set{\top,\bot}\), then \(p\) is a sentence of propositional logic.
- If \(\phi,\psi\) are sentences of propositional logic, then \((\phi \wedge \psi)\) is a sentence of propositional logic.
- If \(\phi,\psi\) are sentences of propositional logic, then \((\phi \vee \psi)\) is a sentence of propositional logic.
- If \(\phi,\psi\) are sentences of propositional logic, then \((\phi \rightarrow \psi)\) is a sentence of propositional logic.
- If \(\phi \) is a sentence of propositional logic, then so is \(\neg \phi \)
1.3. The grammar as a datatype
We'll use a sum type to implement the grammar of propositional logic. We can start with just the following:
data PropL = PVar String deriving (Eq,Show)
ghci> :t (PVar "p1") PropL ghci> :t (PVar "p3") PropL
1.4. Adding negation: recursive datatypes!
data PropL = PVar String | PNot PropL deriving (Eq,Show)
Note that we reuse PropL in the constructor for negative sentences; this means that if an expression p is of type PropL, then PNot p is of type PropL.
ghci> :t (PNot (PVar "p1")) PropL ghci> :t (PNot (PNot (PNot (PVar "p1"))) PropL
Note that this already gives us infinite inhabitants of type PropL.
1.5. Adding connectives
data PropL = PVar String | PNot PropL | PAnd PropL PropL | POr PropL PropL deriving (Eq,Show)
ghci> :t (PAnd (PVar "p1") (POr (PVar "p2") (PVar "p3"))) PropL ghci> :t (PAnd (PVar "p1")) error
1.6. Bonus: infix constructors!
Functions that take two arguments can be used as infix operators by enclosing them in backticks. This also goes for constructors:
ghci> :t PAnd PropL -> PropL -> PropL ghci> :t ((PVar "p1") `PAnd` (PVar "p2")) PropL
You can even use infix constructors in the data declaration:
data PropL = PVar String | PNot PropL | PropL `PAnd` PropL | PropL `POr` PropL deriving (Eq,Show)
1.7. Implementing a custom Show instance
Implementing a custom Show instance for PropL simply amounts to defining a function show of type PropL -> String.
data PropL = PVar String | PNot PropL | PropL `PAnd` PropL | PropL `POr` PropL deriving Eq instance Show PropL where show (PVar s) = s show (PNot p) = "~" ++ show p show (p `PAnd` q) = "(" ++ show p ++ " & " ++ show q ++ ")" show (p `POr` q) = "(" ++ show p ++ " | " ++ show q ++ ")"
1.8. Custom Show cont.
The Show instance we just declared will automatically be used by ghci.
ghci> ((PVar "p1") `PAnd` (PNot ((PVar "p1") `POr` (PVar "p3"))) (p1 & ~(p2 | p3))
We can also use it explicitly by calling show on something of type PropL.
1.9. ASTs
Recursive datatypes are used to create an Abstract Syntax Tree (AST) for sentences of propositional logic.
1.10. Recursive functions
Let's say that we want to compute the number of operators in a formula. In order to do so we'll need a recursive function opsNr.
First, we define the base of the recursion (where the recursion halts):
opsNr :: PropL -> Int opsNr (PVar _) = 0
1.11. Recursive functions cont.
For all other cases we need recursion:
opsNr :: PropL -> Int opsNr (PVar _) = 0 opsNr (PNot p) = 1 + opsNr p opsNr (PAnd p q) = 1 + opsNr p + opsNr q opsNr (POr p q) = 1 + opsNr p + opsNr q
1.12. Another recursive function: depth
depth :: PropL -> Int depth (PVar _) = 0 depth (PNot p) = 1 + depth p opsNr (PAnd p q) = undefined opsNr (POr p q) = undefined
1.13. Depth cont.
depth :: PropL -> Int depth (PVar _) = 0 depth (PNot p) = 1 + depth p depth (PAnd p q) = 1 + max (depth p) (depth q) depth (POr p q) = 1 + max (depth p) (depth q)
1.14. Exercise: gather names
- Exercise: write a recursive function that returns a list of all of the variables that occur in a formula.
- As a bonus, remove duplicates and sort the output alphabetically.
1.15. Normalization
- A formula of propositional logic is in conjunctive normal form (CNF) iff it is a conjunction of one of more clauses.
- A clause is a disjunction of one or more literals; a literal is either a propositional variable, or the negation of a propositional variable.
- Some formulas in conjunctive normal form:
- \((p \vee \neg q \vee r) \wedge s\)
- \((p \vee q) \wedge r\)
- \(p\)
1.16. Conversion to CNF and rules of inference
- Any formula can be converted into CNF by successively applying the following rules of inference:
- \(\neg \neg \phi \Rightarrow \phi \) (Double Negation Elimination; DNE)
- \(\neg (\phi \vee \psi) \Rightarrow \neg \phi \wedge \neg \psi\) (De Morgan's law 1; dM1)
- \(\neg (\phi \wedge \psi) \Rightarrow \neg \phi \vee \neg \psi\) (De Morgan's law 2; dM1)
- \(\phi \vee (\psi \wedge \rho) \Rightarrow (\phi \vee \psi) \wedge (\phi \vee \rho)\) (Distributive Law; DL)
- Conversion to CNF can be accomplished by:
- Pushing negations in, by repeatedly applying dM.
- Getting rid of any double negations via DNE.
- Repeatedly applying DL, to get rid of disjunctions applying over conjunctions.
1.17. An example
\[\neg (\neg (p \vee q) \wedge r) \wedge \neg (p \wedge r)\]
\[\begin{aligned}[t] &\Rightarrow (\neg \neg (p \vee q) \vee r) \wedge \neg (p \wedge r)\\ &\Rightarrow (\neg \neg (p \vee q) \vee r) \wedge (\neg p \vee \neg r)\\ &\Rightarrow (p \vee q \vee r) \wedge (\neg p \vee \neg r) \end{aligned}\]
1.18. Implementation in Haskell
- Let's try to write a function to implement this procedure in Haskell.
- We know what the type of this function should be:
toCNF :: PropL -> PropL toCNF = undefined
1.19. Pushing negation in
We'll break this function down into three steps.
- First, let's push negations inward by repeatedly applying dM.
- Second, let's get rid of any resulting double negations.
- Third, let's distribute conjunctions over disjunctions.
toCNF :: PropL -> PropL toCNF = distributeConj . elimDN . pushNegsIn dM :: PropL -> PropL dM :: PropL -> PropL dne :: PropL -> PropL dne = undefined distLaw :: PropL -> PropL distLaw = undefined
1.20. First step: de Morgan's
We can apply de Morgan's via heavy use of pattern matching.
dM :: PropL -> PropL dM (PNot (p `POr` q)) = (PNot p) `PAnd` (PNot q) dM p = p
Pattern matching applies wherever possible, making the second line an elsewhere case.
1.21. de Morgan's continued
dM :: PropL -> PropL dM (PNot (p `PAnd` q)) = (PNot p) `POr` (PNot q) dM (PNot (p `POr` q)) = (PNot p) `PAnd` (PNot q) dM p = p
1.22. Non-recursive
- Note that the function we just defined is not recursive.
- It only applies de Morgan's if the top level formula matches the structural description imposed by pattern matching.
ghci> dM (PVar "p1" `PAnd` PNot (PVar "p2" `POr` PVar "p3")) (p1 & ~(p2 | p3))
- In order to eventually convert to CNF we need to apply
dMrecursively. - Exercise: write a recursive variant of
dM.
1.23. Recursive application of de Morgan's
dM :: PropL -> PropL dM (PNot (p `PAnd` q)) = (PNot (dM p)) `POr` (PNot (dM q)) dM (PNot (p `POr` q)) = (PNot (dM p)) `PAnd` (PNot (dM q)) dM (PNot p) = PNot (dM p) dM (p `PAnd` q) = dM p `PAnd` dM q dM (p `POr` q) = dM p `POr` dM q dM (PVar p) = PVar p
1.24. Double Negation Elimination
- Double negation elimination is a bit simpler.
- First, the non-recursive variant:
dne :: PropL -> PropL dne (PNot (PNot p)) = p dne p = p
- Exercise: make this apply recursively
1.25. Recursive DNE
dne :: PropL -> PropL dne (PNot (PNot p)) = dne p dne (PNot p) = PNot (dne p) dne (p `PAnd` q) = dne p `PAnd` dne q dne (p `POr` q) = dne p `POr` dne q dne (PVar p) = PVar p
1.26. Distributive Law
First, the non-recursive variant (we're really stretching the limits of pattern matching):
Our statement of the distributive law actually subsumes three different cases:
distLaw :: PropL -> PropL distLaw ((p `PAnd` q) `POr` (r `PAnd` s)) = (p `POr` r) `PAnd` (p `POr` s) `PAnd` (q `POr` r) `PAnd` (q `POr` s) -- double distributivity distLaw (p `POr` (q `PAnd` r)) = (p `POr` q) `PAnd` (p `POr` r) --left dist distLaw ((q `PAnd` r) `POr` p) = (q `POr` p) `PAnd` (r `POr` p) --right dist distLaw p = p
- Exercise: write the recursive variant!
1.27. Recursive distributive law
distLaw :: PropL -> PropL distLaw ((p `PAnd` q) `POr` (r `PAnd` s)) = (distLaw p `POr` distLaw r) `PAnd` (distLaw p `POr` distLaw s) `PAnd` (distLaw q `POr` distLaw r) `PAnd` (distLaw q `POr` distLaw s) -- double distributivity distLaw (p `POr` (q `PAnd` r)) = (distLaw p `POr` distLaw q) `PAnd` (distLaw p `POr` r) --left dist distLaw ((q `PAnd` r) `POr` p) = (distLaw q `POr` distLaw p) `PAnd` (distLaw r `POr` distLaw p) --right dist distLaw (PNot p) = PNot (distLaw p) distLaw (p `PAnd` q) = distLaw p `PAnd` distLaw q distLaw (p `POr` q) = distLaw p `POr` distLaw q distLaw (PVar p) = PVar p
1.28. Mini assignment
- A function that maps formulas of propositional logic to a truth table.
- You can treat a truth table as a list of pairs of variable assignments and truth values.
- A variable assignment is a list of pairs of variables and truth values.
1.29.
\(\mathscr{Fin}\)