Logic in Diesel
For this part of the homework, to gain practice programming in Diesel and
using Vortex, you will develop an implementation of propositional logic
formulae (i.e., boolean expressions) in Diesel. Your
implementation should
exploit a number of Diesel language features, as well as including some
interesting algorithms and representation choices.
The logic.diesel file is a
starter file defining the
interface to Formula objects; you should flesh it out with a real
implementation. The logic-tests.diesel
file
contains an initial set of tests for your implementation; sample output
of
running logic-tests.diesel on my sample solution is here. You should also add your own
tests to logic-tests.diesel.
API
There are several different kinds of logic formulae that can be
constructed:
- true and false literals (named True and False)
- boolean variables (created by invoking Variable(name),
where name is the name of the variable)
- negations (created by invoking Not(f), where f
is some formula)
- conjunctions (created by invoking And(f1, f2),
where f1 and f2 are some formulae)
- disjunctions (created by invoking Or(f1, f2),
where f1 and f2 are some formulae)
- implications (created by invoking Implies(f1, f2),
where f1 and f2 are some formulae,
meaning that f1 implies f2)
All of these expressions yield an object of type Formula
(or some subtype of
Formula).
The following operations are defined on Formula objects
(in addition to the
formula "constructors" above):
- =(Formula, Formula):bool
returns whether two formulae are structurally equal
- print_string(Formula):string
returns a printable string representation of the formula
- evaluate(Formula, bindings:table[string,bool]):bool
returns whether the formula evaluates to true or false, where bindings
specifies the boolean values of (at least) the variables in the formula
(evaluate is allowed to fail if any variables mentioned in the
formula are not given values in bindings)
- varsDo(Formula, &(string):void):void
invoke the argument closure on the name of each variable occurring in
the formula; if a variable appears in the formula more than once,
the closure will be invoked with that variable's name more than once.
Optionally, there is an additional operation
available for
formulae:
- isTautology(Formula):bool
return whether the formula is always true, no matter what bindings are
given to the variables in it; for example, And(Variable("a"),Not(Variable("a"))) is a classic tautology, the "law of the
excluded middle."
Canonical Representation
Formulae must be represented internally in canonical form.
- Implications must be represented using negations and disjunctions
(there is no direct representation of an implication):
- Implies(A, B) should be represented
as Or(Not(A), B).
- Disjunctions cannot have disjunctions as their second argument;
this forces disjunctions to be written in left-associative form:
- Or(A, Or(B,C)) should be
represented as Or(Or(A,B), C).
- Conjunctions cannot have disjunctions as either argument, and
cannot have conjunctions as their second argument:
- And(Or(A,B), C) should be
represented as Or(And(A,C), And(B,C));
- And(A, Or(B,C)), if A is
not a disjunction, should be
represented as Or(And(A,B), And(A,C));
- And(A, And(B,C)) should be
represented as And(And(A,B), C).
- Negations can have neither disjunctions nor conjunctions as
arguments:
- Not(And(A,B)) should be represented as
Or(Not(A), Not(B));
- Not(Or(A,B)) should be represented as And(Not(A),
Not(B)).
(Note how the Ands and Ors swap roles when
negations are pushed inside them.)
Implementation Notes
You should use good Diesel style in your implementation. You
should use
type declarations for function, method, and field arguments and results, and you
should be
able to successfully typecheck your implementation. Several
features of Diesel will be helpful, including multimethods and easy
support for
one-of-a-kind objects. Explicit tests like if should be
used rarely, if at
all; method dispatching should take care of any "instanceof tests".
Here are some suggestions:
- You should probably have named objects for True
and False, and concrete classes for VariableFormula,
NegationFormula, ConjunctionFormula, and DisjunctionFormula.
These can be organized into a suitable inheritance hierarchy below abstract
class Formula.
- The canonical form rules suggest introducing abstract classes like NegationOrSimplerFormula and ConjunctionOrSimplerFormula,
where true, false, and variables are "simpler" than negations,
which in turn are simpler than conjunctions, which in turn are simpler than disjunctions. LiteralFormula (for True and False)
and AtomicFormula (for literals and variables) might also be
useful notions to help in organizing the hierarchy.
- The = operation can have a default implementation for a
pair of Formulae that returns false, which is overridden by a
multimethod for each Formula descendant that might return
true. E.g., in my sample solution, I have the following
multimethod:
method =(f1@ConjunctionFormula, f2@ConjunctionFormula):bool {
f1.arg1 = f2.arg1 & { f1.arg2 = f2.arg2 } }
- The "constructor" operations like Not, And,
and Or need to identify whether their arguments are in the
required form, and if not, what to do about it. The best way to do this
is to define several (multi)methods that dispatch differently, and
handle the various cases. E.g., in my sample solution, I have the
following Or methods:
public fun Or(:Formula, :Formula):Formula;
method Or(f1:Formula, f2@ConjunctionOrSimplerFormula):Formula {
concrete object isa DisjunctionFormula { arg1 := f1, arg2 := f2 } }
method Or(f1:Formula, f2@DisjunctionFormula):Formula {
Or(Or(f1, f2.arg1), f2.arg2) }
The former method handles the case where the arguments are already in
the required form, while the latter method handles the rule that Or(A,
Or(B,C)) should be represented as Or(Or(A,B),
C).
- To test whether a formula is a tautology, one can first compute
the set of unique names used in the formula, then construct all
possible combinations of true and false for that many variables, and then for each combination construct an assignment table mapping each unique
variable name to some boolean value, and evaluate the formula under
that assignment. If all assignments lead to the formula being
true, then it's a tautology.