Homework 6
Due: Saturday, Feb 21st at 11:59pm
Start with the template code here.
- Replace each
failwith "TODO"with your solution. - Be sure to test your solutions in
utopor in tests alongside your submission. To load your file intoutop, use#use "hw6.ml";;. - Submit your version of
hw6.mlto the HW6 assignment on Gradescope. Only submithw6.ml; NOT any other files or folders. - At the top of the template file, there is a comment for you to write in approximately how many hours you spent on the assignment. Filling this in is optional, but helpful feedback for the instructors.
Data representations and PBT
Recall from homework 1 that we introduced propositional logic to you, which can be naturally represented in OCaml as expressions of type bool:
We do not have an implication operation, but we can easily implement it either as a function, or even as an operator.
For this assignment we are providing a local Pbt module which includes helper functions that you can use when working with property-based testing. This will provide to you with forall as well as some combinators and simple generators which you can use when property based testing:
let test_equiv_p1_p2 () =
forall (gen3 gen_bool gen_bool gen_bool) (fun (a, b, c) -> p1 a b c = p2 a b c)
let test_equiv_impl () =
forall (gen2 gen_bool gen_bool) (fun (a, b) -> impl a b = a ==> b)
Just as we can define implication, we could have defined and if it did not
already exist, so the choice of true and false, while convenient, was in
some sense arbitrary.
Indeed, we could have, and in this set of problems, you will, define propositional truth values using many different representations, all of which work. For each, we will define a common set of operations, and use an abstract data type to create a common interface such that, in the end, you can use all inter-changably. First, though, we’ll define a non-trivial program that uses an abstract interface for our truth values.
Question 1
First, we will provide the TruthVal interface, for propositional truth values.
This includes two base values, called tru and fls, three operations tand,
tor, and tnot, and a way of asking if a value is "truthy" or "falsy" (so
that, when we are done with dealing with abstract propositional values, we can
move back to regular computation).
module type TruthVal = sig
type t
val tru : t
val fls : t
val tand : t -> t -> t
val tor : t -> t -> t
val tnot : t -> t
val tif : t -> 'a -> 'a -> 'a
end
Implement TruthVal using booleans, and fill in the GenTruthVal module to generate arbitrary TruthVal values (tru/fls) as well as all possible expressions in the signature (when generating tif, return type t for the polymorphic 'a). As this will be a recursive function, we allow for an optional argument maxdepth that indicates the deepest level of recursion before return a terminal expression of tru or fls.
module BoolTruthVal : TruthVal = struct ... end
module GenTruthVal (T : TruthVal) = struct
let rec gen ?(maxdepth : int = 20) : unit -> T.t = failwith "TODO"
end
Note
4 points (autograded) for the implementatino of BoolTruthVal
1 point (manually graded) for the implementatino of GenTruthVal
Question 2
Now, we want to use our TruthVal. In this sequence of problems, we will make
a naive (i.e., brute force) SAT solver. We will take, as input, formulas in
conjunctive normal form
(CNF), i.e., it is a
conjunction (AND) of formulas that are themselves disjunctions (OR) of either
variables or negated variables (called literals):
We’ll represent the variables as natural numbers (imagine they are x0, x1, x2, etc) and we'll define the formulas polymorphic of whether they have variables or a different value (here a clause is a formula of literals OR'd together):
We use lists and do not explicitly represent the AND's or the OR's because of the uniformity: the list of literals (variables/negated variables) in a clause are all OR'd together, and the list of clauses in the CNF are all AND'd together.
Our goal will be to find out if we can find mappings from variables to booleans which will make our formula reduce to a true value. If we can, we say that the formula is satisfiable, "is SAT," or "has a satisfying assignment."
an empty CNF is (trivially) satisfiable and an empty clause is trivially unsatisfiable
To see if a formula is satisfiable, we’ll do several things. First, we need to figure out the variables that occur in our CNF. We’ll do this by calculating a natural number that is greater or equal to all variables in the CNF. If the CNF is empty, this is 0, otherwise, it should be the largest variable you find.
As an example, variable_upper_bound [[Var 0, NotVar 4], [Var 1]] should return 4.
Note
5 points (autograded)
Question 3
Next, we need a way of evaluating CNF formulas, where we’ve already replaced all the variables with abstract truth values. So we want a function with the following signature / starter:
When you’re thinking about how to do this, think how to evaluate the expression (what has to be combined with OR, what with AND). Then, to actually do it, extract the corresponding function that works on the abstract TruthVal that you are given from the API argument.
Note
5 points (autograded)
Question 4
Now we need a way of substituting for variables into CNF formulas. We’ll do
this before calling eval. So we want a function with the following starter code.
Note
that we still need the API, so we can do negation when we substitute for a negated variable.
Note
Assume that the mapping always contains all variables used in the given CNF and that there are no duplicated variables in the mapping. You may want to look at the assoc function of the List module.
type 'a mapping = (variable * 'a) list
module Subst(T : TruthVal) = struct
let subst (vars : T.t mapping) (cnf : variable_clause cnf) : T.t cnf =
failwith "TODO"
end
Note
5 points (autograded)
Question 5
Finally, now that we can substitute and evaluate formulas (i.e., see if it evaluates to true or false), we need to be able to enumerate all possible assignments and check if there exists one that results in truth: this is the SAT problem.
We’re providing you, as a helper function all_assignments which, given an
arbitrary T and F value, and a maximum Variable (a Natural number),
enumerates all possible combinations of the values as a list of lists. These
are essentially the rows of a truth table, that you can then use as
arguments to your earlier functions.
let all_assignments (vals : 'a list) (bound : variable) : (variable * 'a) list list =
let open List in
let rec go n =
if n <= 0 then [[]] else
let xs = go (n - 1) in
let prepend_permutations ys = map (fun v -> (n - 1, v) :: ys) vals in
concat_map prepend_permutations xs
in
go bound
let () =
test_assert "assignments for 3 variables of a or b value" (
all_assignments ["a"; "b"] 3 =
[[(2, "a"); (1, "a"); (0, "a")]; [(2, "b"); (1, "a"); (0, "a")];
[(2, "a"); (1, "b"); (0, "a")]; [(2, "b"); (1, "b"); (0, "a")];
[(2, "a"); (1, "a"); (0, "b")]; [(2, "b"); (1, "a"); (0, "b")];
[(2, "a"); (1, "b"); (0, "b")]; [(2, "b"); (1, "b"); (0, "b")]])
module Sat (T : TruthVal) = struct
module E = Eval(T)
module S = Subst(T)
let sat (cnf : variable_clause cnf) : T.t =
failwith "TODO"
end
Note
10 points (autograded)
Question 6
Since we have written all our code abstractly, we can implement several other implementations of TruthVal. We'd like you define one with 1 and 0 as T and F, respectively; one with strings (use "true" and "false"), and one with custom structs (type t = { value : unit list }).
module IntTruthVal : TruthVal = struct
type t = int
...
end
module StringTruthVal : TruthVal = struct
type t = string
...
end
module RecordTruthVal : TruthVal = struct
type t = { value : unit list }
...
end
Note
3 points (manually graded)
Question 7
Finally, given that we have many different implementations of TruthVal it is
important to know that, not only are these implementing the same specification,
but we have an equivalence of implementation, up to the notation of isomorphism.
An isomorphism signifies a structural similarity, where two types can be
transformed into one another without loss of information. In our context, given
two truth value implementations represented as modules A and B, Implement
the conv function to take an element from A and "convert" it into an element
of B, alonside an "inverse" function inv that maps elements back from B
into A.
An isomorphism is defined by the left and right inverse properties:
prop_leftinvsays \(\(\forall a : A.t, inv (conv a) = a\)\)prop_rightinvsays \(\(\forall b : B.t, conv (inv b) = b\)\)
Implement these properties and complete check_iso to ensure that your
properties are correct.
module TruthValIso (A : TruthVal) (B : TruthVal) = struct
let conv (a : A.t) : B.t = failwith "TODO"
let inv (b : B.t) : A.t = failwith "TODO"
let prop_leftinv (a : A.t) = failwith "TODO"
let prop_rightinv (b : B.t) = failwith "TODO"
module GA = GenTruthVal(A)
module GB = GenTruthVal(B)
let check_iso : (A.t * B.t) option = failwith "TODO"
end
Note
6 points (autograded)