Homework 6
Due: Sunday, October 19 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.
Rings
A ring is a data type \(T\) with five extra pieces of data:
- An element of type \(T\) called
zero; - An element of type \(T\) called
one; - A multiplication function
mult, of type \(T \to T \to T\); and - An addition function
add, of type \(T \to T \to T\); and - A negation function
neg, of type \(T \to T\).
Thus, a ring can be thought of as a generalized form of arithmetic, where the type \(T\) does not need to be integers, but could be something else. However, the operations add and mult should behave as they do in normal arithmetic. This is expressed in the form of ring axioms, but you won't need to understand the ring axioms for this homework.
We can translate this version of a ring into OCaml by writing a module type for it:
module type Ring = sig
type t
val zero : t
val one : t
val mult : t -> t -> t
val add : t -> t -> t
val neg : t -> t
val gen : unit -> t
val equals : t -> t -> bool
end
In addition to zero/one/mult/add/neg, we also include functions gen for
generating a value of type t; and equals, for checking if two values of type
t are equal in the ring.
Q1: Defining Rings
First, we will define some concrete examples of rings. Below is an example:
module IntRing : (Ring with type t = int) = struct
type t = int
let zero = 0
let one = 0
let mult x y = x * y
let add x y = x + y
let neg x = (- x)
let gen () = Random.int 100 - 50 (* Generating both positive and negative integers *)
let equals x y = (x = y)
end
Note that we give IntRing the module type Ring with type t = int. This means
that IntRing should have module type Ring, but we are additionally allowing
the context to know that t = int. Thus, we are making sure the type is not abstract.
The rest of our module annotations will look like this (Ring with type t = ...), but we will supply it for you.
Q1.1
Another example of a ring is the ring of booleans, where: zero is false; one is true; mult is AND; add is XOR; and neg is negation.
Implement this boolean ring below:
equals by =.
Q1.2
An important ring for computer science is the ring of non-negative integers modulo some other number, n. In this ring, zero, one, mult, add, and neg are just like the integers, except taken mod n, for some n.
We can set this up by having a module for the modulus n:
IntMod be a functor (module which takes another module as an argument, like a function):
Thus, your operations inside of IntMod should be modulo M.n.
Crucially, your definition of equals must identify elements that are equal in the ring; not only elements that are represented by the same underlying integer. Two integers represent the same ring element exactly when they
are a multiple of n apart from each other. Thus, if n = 3, we should get that:
For the generator gen, you should only generate elements that are reduced mod n; thus, if n = 3, your generator should not return -5 (since -5 is equal to 1 modulo 3).
Hint
To implement equals, you will need to use OCaml's built-in infix mod operator; however, this alone does not suffice.
See what mod does on negative numbers.
Q2: Building Bigger Rings from Smaller Ones
Q2.1 Given a pair of rings R1 and R2, you can build a bigger one via the product ring R1 * R2, where zero is the pair of zeroes, one is the pair of ones, and each operation is defined componentwise.
We can implement it like so:
Implement it.
Q2.2 Given a ring R, the space of functions from string to R also forms a ring. Similar to the product ring, this ring of functions works by defining zero by fun _ -> 0 (where 0 is defined by the ring \(R\)), and similarly for one.
The operations mult/add/neg are defined componentwise.
Equality in this ring is undecidable, so you can leave equals unimplemented:
For generating a random function, you can use this helper function (included in hw6.ml):
let gen_func (gen : unit -> 'a) : unit -> (string -> 'a) =
let m = Hashtbl.create 100 in
fun _ ->
fun s ->
match Hashtbl.find_opt m s with
| Some v -> v
| None ->
let v = gen () in
Hashtbl.add m s v;
v
'a to a generator for functions from string to 'a. (We do this by using a mutable hash table, but you don't need to fully understand how gen_func works. We will cover mutable state in Week 7.)
module FuncRing (R : Ring) : (Ring with type t = (string -> R.t)) = struct
...
let equals _ _ = failwith "UNIMP"
...
end
Q3: Polynomials over a Ring
Given a ring \(R\), we can construct polynomials with coefficients in \(R\). These are the expressions that are built out of: variables (given by an integer index); elements of the ring \(R\); addition; and multiplication.
You may assume all variables are indexed by non-negative integers.
Q3.1
Given a map from integers to ring elements, we can develop a functor to evaluate polynomials:
module IntMap = Map.Make(Int)
module Poly (R : Ring) = struct
type t = R.t poly
let rec eval (map : R.t IntMap.t) (p : t) : R.t option =
failwith "TODO"
end
Implement eval by recursively traversing the polynomial. It should return None if any variable is not in the map. (To lookup a variable in the map, you should use IntMap.find_opt).
Q3.2
A polynomial \(P\) defined over a ring \(R\) is zero if it is equal to zero on all inputs:
(Again, above, \(0\) is referring to the zero in the ring.)
It turns out to be extremely useful to know if a certain \(P\) is zero. While knowing if it equal to zero on all inputs is very hard (since there are exponentially -- or even unboundedly many -- arguments to check), the Schwarz-Zippel Lemma tells us that to decide if \(P\) is zero, it suffices to randomly test if \(P\) is zero on many different inputs. This is exactly equivalent to just using PBT to check the validity of formula \((1)\)! This is a rare case where PBT has an explicit soundness guarantee (albeit a probabilistic one).
Using your functor Poly, implement a new functor PID that uses PBT to test if a polynomial is zero:
module PID (R : Ring) = struct
module P = Poly(R)
(* Compute maximum variable in the polynomial. *)
let max_var (p : P.t) : int = failwith "TODO"
(* Create a map with keys ranging from 0 to n (inclusive), filled with random ring elements. *)
let build_map (n : int) : R.t IntMap.t = failwith "TODO"
(* Use PBT to test if the polynomial returns zero on all inputs. *)
let test_pid (p : P.t) : bool = failwith "TODO"
(* Using `test_pid`, test if `p` and `q` are identical polynomials. *)
let test_equiv (p : P.t) (q : P.t) : bool = failwith "TODO"
end
- First, implement
max_varwhich computes, for a given polynomial, the highest variable index in the polynomial (remember we are assuming they begin at index \(0\)). - Then, implement
build_mapwhich, given a non-negative integern, computes a value of typeR.t IntMap.t(i.e., a map from integers to ring elements) that is defined on all integers between0andn(includingn), and is filled with random ring elements (generated usingR.gen). - Now, you can implement
test_pidwhich usesmax_varandbuild_mapto actually perform PBT to test ofpis equal to zero on all inputs. Your PBT main loop should run for 1000 trials. Feel free to importforallto do this (included inhw6.ml): - Finally, you can use
test_pidto test if two polynomials are equivalent: if they return the same value on all inputs. Implement this test intest_equiv. (Hint: in a ring, \(x = y\) only if \(x - y = 0\).)
Hint
To implement build_map, first construct a list of key-value pairs (int * R.t) list; then use IntMap.of_list to convert that list to a map. You can construct this list by recursion on the number of variables in the polynomial, constructed by max_var.