Simplification

We can write a program in Racket that will symbolically manipulate a Boolean expression. We will simplify an expression to CNF form.

Preliminaries

We need a few of the functions from our boolean evaluator to make the simplification program.

;Get the first operand of an expression
;input-contract: A valid expression
;output-contact: The first operand
(define (op1 exp)
  (second exp)
)
;Get the second operand of an expression
;input-contract: A valid expression
;output-contact: The second operand
(define (op2 exp)
  (third exp)
)

;Detect Expression Type
;Given an operator and list length
;creates a procedure to detect expressions of that format
;input-contract: (and (symbol? op) (integer? len))
;output-contract: (procedure? (isOperator op len))
(define (isOperator op len)
  (lambda (expr)
    (and
     (list? expr)
     (equal? (length expr) len)
     (equal? op (first expr))
    )
 )
)
;Define the formats we need
;Negation only takes 1 input
;Other Boolean Operators are 2 inputs and the operator
(define isNot? (isOperator '~ 2) )
(define isAnd? (isOperator '& 3) )
(define isOr?  (isOperator 'v 3) )
(define isImplies? (isOperator '-> 3))
(define isBicond? (isOperator '<-> 3))

Negative Normal Form

First, we need to convert the expression to NNF. This is a prerequisite of NNF.

NNF Rules

There were a number of simplification rules used for NNF. We implement a function for each of them.

We need to replace conditional statements with their definition.

\( \begin{align} (A \implies B) = (\neg A \vee B) \end{align} \)

;Rule 1: Definition of Implies
;(A->B) = (~A v B)
;input-contract: A valid implies expression
;output-contract: The expression rewritten as an or
(define (defOfImplies expr)
  (list 'v (list '~ (op1 expr)) (op2 expr))
)
(display "Def of Implies Example:\n")
(display (defOfImplies '(-> X Y)))(newline)
; Returns '(v (~ X) Y)

We also have a definition for the Bi-Conditional.

\( \begin{align} (A \iff B) = (A \implies B) \wedge (B \implies A) \end{align} \)

;Rule 2: Definition of Bi-Conditional
;(A <-> B) = (A->B) & (B->A)
;input-contract: A valid bi-conditional expression
;output-contract: The expression rewritten as an or+implies
(define (defOfBicond expr)
  (list '&
        (list '-> (op1 expr) (op2 expr))
        (list '-> (op2 expr) (op1 expr))
  )
)
(display "Def of Bi-Conditional Example:\n")
(display (defOfBicond '(<-> X Y)))(newline)
; Returns '(v (-> X Y) (-> Y X))

We also need to apply DeMorgan’s. This version works on a disjunction.

\( \begin{align} \neg(A \vee B) = (\neg A) \wedge (\neg B) \end{align} \)

;Rule 3: DeMorgan on Disjunction
;~(A v B) = (~A & ~B)
;input-contract: A valid boolean expression with negative applied to or
;output-contract: The expression rewritten using DeMorgan's Law
(define (deMorganDis expr)
  (list '&
        (list '~ (op1 (op1 expr)))
        (list '~ (op2 (op1 expr)))
  )
)
(display "DeMorgan on Disjunction Example:\n")
(display (deMorganDis '(~ (v X Y))))(newline)
; Returns '(& (~ X) (~ Y))

The DeMorgan’s Law version for conjunctions is also needed.

\( \begin{align} \neg(A \wedge B) = (\neg A) \vee (\neg B) \end{align} \)

;Rule 4: DeMorgan on Conjunction
;~(A & B) = (~A v ~B)
;input-contract: A valid boolean expression with negative applied to and
;output-contract: The expression rewritten using DeMorgan's Law
(define (deMorganConj expr)
  (list 'v
        (list '~ (op1 (op1 expr)))
        (list '~ (op2 (op1 expr)))
  )
)
(display "DeMorgan on Conjunction Example:\n")
(display (deMorganConj '(~ (& X Y))))(newline)
; Returns '(v (~ X) (~ Y))

The last rule we need is double negative elimination.

\( \begin{align} \neg \neg A = A \end{align} \)

;Rule 5: Double Negative Elimination
;~ ~ A = A
;input-contract: A valid boolean expression with a double negative
;output-contract: The expression with double negative removed
(define (doubleNeg expr)
  (op1 (op1 expr))
)
(display "Double Negative Elimination Example:\n")
(display (doubleNeg '(~ (~ (v A B)))))(newline)
; Returns '(v A B)

Is NNF?

To know when we should apply the rules, we need to know if the expression is in NNF form or not. We need to search the expression recursively. If we hit a point where a negation is above another operator, we know the expression is not in NNF.

;Determine if a Function is in NNF form or not
;input-contract: A valid boolean expression
;output-contract: #t if in NNF and #f otherwise
(define (isNNF? expr)
  (cond
    ;These operators are fine, but check operands
    [(or (isAnd? expr) (isOr? expr))
     (and (isNNF? (op1 expr)) (isNNF? (op2 expr)))]
    ;These operators are not allowed
    [(isImplies? expr) #f]
    [(isBicond? expr) #f]
    ;Not cannot be applied to operators
    [(isNot? expr) (not (list? (op1 expr)))]
    ;None of the above
    [else #t]
  )
)

Apply Rules

We have a number of rules. We need to determine which to apply when. This function tests an expression to see if any rules apply. If not, it searches recursively.

;Apply Rules of NNF
;Applies any rules it finds to the expression
;input-contract: A boolean expression
;output-contract: an equivalent boolean expression with rules applied
(define (simplifyNNF expr)
  (cond
    ;Operator Defs
    [(isImplies? expr) (defOfImplies expr)]
    [(isBicond? expr) (defOfBicond expr)]
    ;DeMorgan
    [(and (isNot? expr) (isOr? (op1 expr)))
     (deMorganDis expr)]
    [(and (isNot? expr) (isAnd? (op1 expr)))
     (deMorganConj expr)]
    ;Double Negative
    [(and (isNot? expr) (isNot? (op1 expr)))
     (doubleNeg expr)]
    ;Basic Operators
    [(isNot? expr) (list '~ (simplifyNNF (op1 expr)))]
    [(isAnd? expr) (list '& (simplifyNNF (op1 expr))
                         (simplifyNNF (op2 expr)))]
    [(isOr? expr) (list 'v (simplifyNNF (op1 expr))
                        (simplifyNNF (op2 expr)))]
    ;None of the above
    [else expr]
  )
)

Convert to NNF

To convert an expression to NNF, we just apply the rules repeatedly. If we apply them over and over again, eventually we will make it to NNF form.

;Convert to NNF
;Apply Rules until the expression is in NNF
;input-contract: a valid boolean expression
;output-contract: the same expression in NNF
(define (nnf expr)
  ;(display expr)(newline) ;Uncomment to see steps
  (if (isNNF? expr)
      expr
      (nnf (simplifyNNF expr))
  )
)

(display "Convert to NNF Examples:\n")
(display (nnf '(<-> A B)))(newline)
; Returns '(v (v (~ A) B) (v (~ B) A))
(display (nnf '(~ (<-> A B))))(newline)
; Returns '(& (& A (~ B)) (& B (~ A)))
(display (nnf '(~ (& (<-> A B) (-> (~ C) D)))))(newline)
; Returns '(v (v (& A (~ B)) (& B (~ A))) (& (~ C) (~ D)))
(display (nnf '(~ (& A (v B C)))))(newline)
; Returns (v ~A (& (~ B) (~ C)))

Conjunctive Normal Form

Once and expression is in NNF, we can apply one additional rule to convert it to CNF.

CNF Rules

We need to distribute disjunctions into conjunctions. We don’t need to worry about cases where both sides have a conjunction. That is just two applications of the rule. Notice that computers don’t understand rules can be flipped around the operator. We need to implement both versions.

\( \begin{align} (A \wedge B) \vee C =& (A \vee C) \wedge (B \vee C)\\ A \vee (B \wedge C) =& (A \vee B) \wedge (A \vee C) \end{align} \)

;Rule 1: Distribute Or
;(A & B) v C = (A v C) & (B v C)
;A v (B & C) = (A v B) & (A v C)
;input-contract: A boolean expression that needs to be
;                distributed
;output-contract: The expression after distribution
(define (distributeOr expr)
  (if (isAnd? (op1 expr))
      (list '&
            (list 'v (op1 (op1 expr))
                     (op2 expr))
            (list 'v (op2 (op1 expr))
                  (op2 expr))
      )
      (list '&
            (list 'v (op1 expr)
                     (op1 (op2 expr)))
            (list 'v (op1 expr)
                  (op2 (op2 expr)))
      )
 )
)
(display "Distribution Examples:\n")
(display (distributeOr '(v (& A B) C)))(newline)
; Returns '(& (v A C) (v B C))
(display (distributeOr '(v A (& B C))))(newline)
; Returns '(& (v A B) (v A C))

Is CNF?

We need to check if an expression is in CNF form. This way we know we can stop simplifying.

;Check if an expression is in CNF form
;input-contract: A valid boolean expression
;output-contract: #t if in CNF and #f otherwise
(define (isCNF? expr)
  (cond
    [(not (isNNF? expr)) #f]
    [(and (isOr? expr)
          (or (isAnd? (op1 expr)) (isAnd? (op2 expr))))
     #f]
    [(or (isAnd? expr) (isOr? expr))
     (and (isCNF? (op1 expr)) (isCNF? (op2 expr)))]
    [else #t]
  )

Apply CNF rules

This function checks to see if the CNF rules apply. If they do, it simplifies the expression.

;Apply Rules of CNF Form
;Applies any rules it finds once
;input-contract: a valid boolean expression
;output-contract: the expression simplified using rules
(define (simplifyCNF expr)
  (cond
    [(not (isNNF? expr)) (nnf expr)]
    [(and (isOr? expr)
          (or (isAnd? (op1 expr)) (isAnd? (op2 expr))))
     (distributeOr expr)]
    [(isAnd? expr) (list '&
           (simplifyCNF (op1 expr))
           (simplifyCNF (op2 expr)))]
    [(isOr? expr) (list 'v
           (simplifyCNF (op1 expr))
           (simplifyCNF (op2 expr)))]
    [(isNot? expr) (list '~
           (simplifyCNF (op1 expr)))]
    [else expr]
    )
)

Simplify CNF

To simplify an expression to CNF, we just apply the rules until the expression is in CNF form.

;Convert to CNF
;Repeatedly apply the rules until you reach CNF form
;input-contract: a valid boolean expression
;output-contract: an equivalent expression in CNF Form
(define (cnf expr)
  (if (isCNF? expr)
      expr
      (cnf (simplifyCNF expr))))

(display "CNF Examples:\n")
(display (cnf '(v (& A B) (& C D))))(newline)
; Returns '(& (& (v A C) (v A D)) (& (v B C) (v B D)))
(display (cnf '(~ (<-> A B))))(newline)
; Returns '(& (& (v A B) (v A (~ A))) (& (v (~ B) B) (v (~ B) (~ A))))

Full Code Example

The full file is provided below.

#lang racket
;Mark Boady
;Drexel University 2021

;This code simplifies an expression to CNF.

;----------------------------------------
;Section 1: Basic Predicates and Helpers
;----------------------------------------

;Get the first operand of an expression
;input-contract: A valid expression
;output-contact: The first operand
(define (op1 exp)
  (second exp)
)
;Get the second operand of an expression
;input-contract: A valid expression
;output-contact: The second operand
(define (op2 exp)
  (third exp)
)

;Detect Expression Type
;Given an operator and list length
;creates a procedure to detect expressions of that format
;input-contract: (and (symbol? op) (integer? len))
;output-contract: (procedure? (isOperator op len))
(define (isOperator op len)
  (lambda (expr)
    (and
     (list? expr)
     (equal? (length expr) len)
     (equal? op (first expr))
    )
 )
)
;Define the formats we need
;Negation only takes 1 input
;Other Boolean Operators are 2 inputs and the operator
(define isNot? (isOperator '~ 2) )
(define isAnd? (isOperator '& 3) )
(define isOr?  (isOperator 'v 3) )
(define isImplies? (isOperator '-> 3))
(define isBicond? (isOperator '<-> 3))


;----------------------------------------
;Section 2: Convert to NNF
;----------------------------------------

;Rule 1: Definition of Implies
;(A->B) = (~A v B)
;input-contract: A valid implies expression
;output-contract: The expression rewritten as an or
(define (defOfImplies expr)
  (list 'v (list '~ (op1 expr)) (op2 expr))
)
(display "Def of Implies Example:\n")
(display (defOfImplies '(-> X Y)))(newline)
; Returns '(v (~ X) Y)

;Rule 2: Definition of Bi-Conditional
;(A <-> B) = (A->B) & (B->A)
;input-contract: A valid bi-conditional expression
;output-contract: The expression rewritten as an or+implies
(define (defOfBicond expr)
  (list '&
        (list '-> (op1 expr) (op2 expr))
        (list '-> (op2 expr) (op1 expr))
  )
)
(display "Def of Bi-Conditional Example:\n")
(display (defOfBicond '(<-> X Y)))(newline)
; Returns '(v (-> X Y) (-> Y X))

;Rule 3: DeMorgan on Disjunction
;~(A v B) = (~A & ~B)
;input-contract: A valid boolean expression with negative applied to or
;output-contract: The expression rewritten using DeMorgan's Law
(define (deMorganDis expr)
  (list '&
        (list '~ (op1 (op1 expr)))
        (list '~ (op2 (op1 expr)))
  )
)
(display "DeMorgan on Disjunction Example:\n")
(display (deMorganDis '(~ (v X Y))))(newline)
; Returns '(& (~ X) (~ Y))

;Rule 4: DeMorgan on Conjunction
;~(A & B) = (~A v ~B)
;input-contract: A valid boolean expression with negative applied to and
;output-contract: The expression rewritten using DeMorgan's Law
(define (deMorganConj expr)
  (list 'v
        (list '~ (op1 (op1 expr)))
        (list '~ (op2 (op1 expr)))
  )
)
(display "DeMorgan on Conjunction Example:\n")
(display (deMorganConj '(~ (& X Y))))(newline)
; Returns '(v (~ X) (~ Y))

;Rule 5: Double Negative Elimination
;~ ~ A = A
;input-contract: A valid boolean expression with a double negative
;output-contract: The expression with double negative removed
(define (doubleNeg expr)
  (op1 (op1 expr))
)
(display "Double Negative Elimination Example:\n")
(display (doubleNeg '(~ (~ (v A B)))))(newline)
; Returns '(v A B)

;Determine if a Function is in NNF form or not
;input-contract: A valid boolean expression
;output-contract: #t if in NNF and #f otherwise
(define (isNNF? expr)
  (cond
    ;These operators are fine, but check operands
    [(or (isAnd? expr) (isOr? expr))
     (and (isNNF? (op1 expr)) (isNNF? (op2 expr)))]
    ;These operators are not allowed
    [(isImplies? expr) #f]
    [(isBicond? expr) #f]
    ;Not cannot be applied to operators
    [(isNot? expr) (not (list? (op1 expr)))]
    ;None of the above
    [else #t]
  )
)

;Apply Rules of NNF
;Applies any rules it finds to the expression
;input-contract: A boolean expression
;output-contract: an equivalent boolean expression with rules applied
(define (simplifyNNF expr)
  (cond
    ;Operator Defs
    [(isImplies? expr) (defOfImplies expr)]
    [(isBicond? expr) (defOfBicond expr)]
    ;DeMorgan
    [(and (isNot? expr) (isOr? (op1 expr)))
     (deMorganDis expr)]
    [(and (isNot? expr) (isAnd? (op1 expr)))
     (deMorganConj expr)]
    ;Double Negative
    [(and (isNot? expr) (isNot? (op1 expr)))
     (doubleNeg expr)]
    ;Basic Operators
    [(isNot? expr) (list '~ (simplifyNNF (op1 expr)))]
    [(isAnd? expr) (list '& (simplifyNNF (op1 expr))
                         (simplifyNNF (op2 expr)))]
    [(isOr? expr) (list 'v (simplifyNNF (op1 expr))
                        (simplifyNNF (op2 expr)))]
    ;None of the above
    [else expr]
  )
)


;Convert to NNF
;Apply Rules until the expression is in NNF
;input-contract: a valid boolean expression
;output-contract: the same expression in NNF
(define (nnf expr)
  ;(display expr)(newline) ;Uncomment to see steps
  (if (isNNF? expr)
      expr
      (nnf (simplifyNNF expr))
  )
)

(display "Convert to NNF Examples:\n")
(display (nnf '(<-> A B)))(newline)
; Returns '(v (v (~ A) B) (v (~ B) A))
(display (nnf '(~ (<-> A B))))(newline)
; Returns '(& (& A (~ B)) (& B (~ A)))
(display (nnf '(~ (& (<-> A B) (-> (~ C) D)))))(newline)
; Returns '(v (v (& A (~ B)) (& B (~ A))) (& (~ C) (~ D)))
(display (nnf '(~ (& A (v B C)))))(newline)
; Returns (v ~A (& (~ B) (~ C)))


;----------------------------------------
; Section 3: Convert CNF
;----------------------------------------

;Rule 1: Distribute Or
;(A & B) v C = (A v C) & (B v C)
;A v (B & C) = (A v B) & (A v C)
;input-contract: A boolean expression that needs to be
;                distributed
;output-contract: The expression after distribution
(define (distributeOr expr)
  (if (isAnd? (op1 expr))
      (list '&
            (list 'v (op1 (op1 expr))
                     (op2 expr))
            (list 'v (op2 (op1 expr))
                  (op2 expr))
      )
      (list '&
            (list 'v (op1 expr)
                     (op1 (op2 expr)))
            (list 'v (op1 expr)
                  (op2 (op2 expr)))
      )
 )
)
(display "Distribution Examples:\n")
(display (distributeOr '(v (& A B) C)))(newline)
; Returns '(& (v A C) (v B C))
(display (distributeOr '(v A (& B C))))(newline)
; Returns '(& (v A B) (v A C))

;Check if an expression is in CNF form
;input-contract: A valid boolean expression
;output-contract: #t if in CNF and #f otherwise
(define (isCNF? expr)
  (cond
    [(not (isNNF? expr)) #f]
    [(and (isOr? expr)
          (or (isAnd? (op1 expr)) (isAnd? (op2 expr))))
     #f]
    [(or (isAnd? expr) (isOr? expr))
     (and (isCNF? (op1 expr)) (isCNF? (op2 expr)))]
    [else #t]
  )
)

;Apply Rules of CNF Form
;Applies any rules it finds once
;input-contract: a valid boolean expression
;output-contract: the expression simplified using rules
(define (simplifyCNF expr)
  (cond
    [(not (isNNF? expr)) (nnf expr)]
    [(and (isOr? expr)
          (or (isAnd? (op1 expr)) (isAnd? (op2 expr))))
     (distributeOr expr)]
    [(isAnd? expr) (list '&
           (simplifyCNF (op1 expr))
           (simplifyCNF (op2 expr)))]
    [(isOr? expr) (list 'v
           (simplifyCNF (op1 expr))
           (simplifyCNF (op2 expr)))]
    [(isNot? expr) (list '~
           (simplifyCNF (op1 expr)))]
    [else expr]
    )
)

;Convert to CNF
;Repeatedly apply the rules until you reach CNF form
;input-contract: a valid boolean expression
;output-contract: an equivalent expression in CNF Form
(define (cnf expr)
  (if (isCNF? expr)
      expr
      (cnf (simplifyCNF expr))))

(display "CNF Examples:\n")
(display (cnf '(v (& A B) (& C D))))(newline)
; Returns '(& (& (v A C) (v A D)) (& (v B C) (v B D)))
(display (cnf '(~ (<-> A B))))(newline)
; Returns '(& (& (v A B) (v A (~ A))) (& (v (~ B) B) (v (~ B) (~ A))))

Conclusion

Any boolean expression can be converted to CNF form. This is a purely mechanical process. It can be implemented recursively. We can let a computer apply the rules for us. This code every produces a value as a result. It takes symbolic expressions and rewrites them into different but equivalent expressions.