I saw How to convert a propositional formula to conjunctive normal form (CNF)? but it doesn't go into implementation details. So I was lucky to find this which shows the types:
abstract class Formula { }
class Variable extends Formula { String varname; }
class AndFormula extends Formula { Formula p; Formula q; } // conjunction
class OrFormula extends Formula { Formula p; Formula q; } // disjunction
class NotFormula extends Formula { Formula p; } // negation
class ImpliesFormula extends Formula { Formula p; Formula q; } // if-then
class EquivFormula extends Formula { Formula p; Formula q; }
class XorFormula extends Formula { Formula p; Formula q; }
Then it has this helpful (start of a) function CONVERT:
CONVERT(φ): // returns a CNF formula equivalent to φ
// Any syntactically valid propositional formula φ must fall into
// exactly one of the following 7 cases (that is, it is an instanceof
// one of the 7 subclasses of Formula).
If φ is a variable, then:
return φ.
// this is a CNF formula consisting of 1 clause that contains 1 literal
If φ has the form P ^ Q, then:
CONVERT(P) must have the form P1 ^ P2 ^ ... ^ Pm, and
CONVERT(Q) must have the form Q1 ^ Q2 ^ ... ^ Qn,
where all the Pi and Qi are disjunctions of literals.
So return P1 ^ P2 ^ ... ^ Pm ^ Q1 ^ Q2 ^ ... ^ Qn.
If φ has the form P v Q, then:
CONVERT(P) must have the form P1 ^ P2 ^ ... ^ Pm, and
CONVERT(Q) must have the form Q1 ^ Q2 ^ ... ^ Qn,
where all the Pi and Qi are dijunctions of literals.
So we need a CNF formula equivalent to
(P1 ^ P2 ^ ... ^ Pm) v (Q1 ^ Q2 ^ ... ^ Qn).
So return (P1 v Q1) ^ (P1 v Q2) ^ ... ^ (P1 v Qn)
^ (P2 v Q1) ^ (P2 v Q2) ^ ... ^ (P2 v Qn)
...
^ (Pm v Q1) ^ (Pm v Q2) ^ ... ^ (Pm v Qn)
If φ has the form ~(...), then:
If φ has the form ~A for some variable A, then return φ.
If φ has the form ~(~P), then return CONVERT(P). // double negation
If φ has the form ~(P ^ Q), then return CONVERT(~P v ~Q). // de Morgan's Law
If φ has the form ~(P v Q), then return CONVERT(~P ^ ~Q). // de Morgan's Law
If φ has the form P -> Q, then:
Return CONVERT(~P v Q). // equivalent
If φ has the form P <-> Q, then:
Return CONVERT((P ^ Q) v (~P ^ ~Q)).
If φ has the form P xor Q, then:
Return CONVERT((P ^ ~Q) v (~P ^ Q)).
I translated it to JavaScript below, but am stuck on the AND and OR bits. I want to make sure I get this correct too.
The description of my "data model" / data structure is here.
/*
* Any syntactically valid propositional formula φ must fall into
* exactly one of the following 7 cases (that is, it is an instanceof
* one of the 7 subclasses of Formula).
*
* @see https://www.cs.jhu.edu/~jason/tutorials/convert-to-CNF.html
*/
function convert(formula) {
switch (formula.type) {
case 'variable': return formula
case 'conjunction': return convertConjunction(formula)
case 'disjunction': return convertDisjunction(formula)
case 'negation': return convertNegation(formula)
case 'conditional': return convertConditional(formula)
case 'biconditional': return convertBiconditional(formula)
case 'xor': return convertXOR(formula)
default:
throw new Error(`Unknown formula type ${formula.type}.`)
}
}
function convertConjunction(formula) {
return {
type: 'conjunction',
base: convert(formula.base),
head: convert(formula.head),
}
// CONVERT(P) must have the form P1 ^ P2 ^ ... ^ Pm, and
// CONVERT(Q) must have the form Q1 ^ Q2 ^ ... ^ Qn,
// where all the Pi and Qi are disjunctions of literals.
// So return P1 ^ P2 ^ ... ^ Pm ^ Q1 ^ Q2 ^ ... ^ Qn.
}
function convertDisjunction(formula) {
return {
type: 'disjunction',
base: convert(formula.base),
head: convert(formula.head),
}
// CONVERT(P) must have the form P1 ^ P2 ^ ... ^ Pm, and
// CONVERT(Q) must have the form Q1 ^ Q2 ^ ... ^ Qn,
// where all the Pi and Qi are dijunctions of literals.
// So we need a CNF formula equivalent to
// (P1 ^ P2 ^ ... ^ Pm) v (Q1 ^ Q2 ^ ... ^ Qn).
// So return (P1 v Q1) ^ (P1 v Q2) ^ ... ^ (P1 v Qn)
// ^ (P2 v Q1) ^ (P2 v Q2) ^ ... ^ (P2 v Qn)
// ...
// ^ (Pm v Q1) ^ (Pm v Q2) ^ ... ^ (Pm v Qn)
}
function convertNegation(formula) {
// If φ has the form ~A for some variable A, then return φ.
if (formula.formula.type === 'variable') {
return formula
}
// If φ has the form ~(~P), then return CONVERT(P). // double negation
if (formula.formula.type === 'negation') {
return convert(formula.formula.formula)
}
// If φ has the form ~(P ^ Q), then return CONVERT(~P v ~Q). // de Morgan's Law
if (formula.formula.type === 'conjunction') {
return convert({
type: 'disjunction',
base: {
type: 'negation',
formula: formula.formula.base,
},
head: {
type: 'negation',
formula: formula.formula.head,
}
})
}
// If φ has the form ~(P v Q), then return CONVERT(~P ^ ~Q). // de Morgan's Law
if (formula.formula.type === 'disjunction') {
return convert({
type: 'conjunction',
base: {
type: 'negation',
formula: formula.formula.base
},
head: {
type: 'negation',
formula: formula.formula.head
}
})
}
throw new Error(`Invalid negation.`)
}
function convertConditional(formula) {
// Return CONVERT(~P v Q). // equivalent
return convert({
type: 'disjunction',
base: {
type: 'negation',
formula: formula.base,
},
head: formula.head
})
}
function convertBiconditional(formula) {
// Return CONVERT((P ^ Q) v (~P ^ ~Q)).
return convert({
type: 'disjunction',
base: {
type: 'conjunction',
base: formula.base,
head: formula.head,
},
head: {
type: 'conjunction',
base: {
type: 'negation',
formula: formula.base,
},
head: {
type: 'negation',
formula: formula.head,
},
}
})
}
function convertXOR(formula) {
// CONVERT((P ^ ~Q) v (~P ^ Q)).
return convert({
type: 'disjunction',
base: {
type: 'conjunction',
base: formula.base,
head: {
type: 'negation',
formula: formula.head,
},
},
head: {
type: 'conjunction',
base: {
type: 'negation',
formula: formula.base,
},
head: formula.head,
}
})
}
I have the AND and OR as a pair. So if you write in math like this:
A ∧ B ∧ C ∧ D ∧ E
That would be more like this in code:
A ∧ (B ∧ (C ∧ (D ∧ E)))
But the problem is, we might have arbitrary trees of formulas:
(((A ∧ B) ∧ (C ∧ D)) ∧ (E ∧ F))
Same with the OR. So how would you implement these functions convertDisjunction and convertConjunction, so they can handle that sort of tree data structure?
I tried implementing convertConjunction and convertDisjunction, but I don't think I have it right.
Working around the problem
The problem you raised about the nested conjunction expressions can be worked around by allowing a formula instance to have more than the
baseandheadoperands. I would suggest to allow conjunctions and disjunctions to have any number of operands and to store them in an array. SoA^B^C^Dwould be just one conjunction.I provide an implementation below. It defines one global class
Formulawhich incorporates the sub classes as static members.A
Formulainstance should be considered immutable. All methods that return a formula either return an existing formula without change, or will create a new formula.Example use
There should be no need to use
new. The program does not have to be aware of the sub classes. The static methods onFormula(namelygetVariableandparse) will give you a first formula instance, from which other formulas can be produced.Be aware that making a CNF can result in large expressions. This code does not attempt to reduce the size by applying many of the rules available for propositional formulas. The returned CNF will always be a conjunction of disjunctions, without simplification. So even when the formula is just a single variable, calling
.cnf()on it will turn it into a conjunction of one argument, which in turn is a disjunction of just one argument. Its string representation will still be the variable's name, as the stringification will not add parentheses for connectives which only have one argument.Implementation
Alternative implementation with
baseandheadThe rules you have listed are valid when expressions have nested con/disjunctions. Due to the recursive nature of some of the rules (where the operands are first converted to CNF, and then the top-level connective is converted), the tree's height will gradually be trimmed down as the recursion tracks back.
You indicated in comments you prefer:
So here is tested code that takes these wishes into account: