Homework 5: Modules and Invariants
Due: Sunday, October 12 11:59pm
Submission Instructions
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 "hw5.ml";;. - Submit your version of
hw5.mlto the HW4 assignment on Gradescope. Only submithw5.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.
Q1: An abstract type for non-empty lists
There are many applications where one wants to store a list of items, but we don't want the list to ever be empty. For example, think of a type of paths in a graph: we can represent this as a list of vertices, but since the path always has an origin, the list is never empty.
One way to do this is to implement a new type, nelist:
While this can work, the main issue is that we then have to re-implement all of the list primitives on nelist (e.g., map, fold, and so on), which duplicates a lot of effort.
Additionally, we might want to cast a nelist back into a list, which would require traversing the whole list to recursively build up the list.
Instead, we will implement non-empty lists as an abstract type, whose implementation is a wrapper around an ordinary list. Below is our signature for the corresponding module:
module type NonEmptySig = sig
type 'a t
val well_formed : 'a t -> bool
val mk : 'a -> 'a t
val cons : 'a -> 'a t -> 'a t
val uncons : 'a t -> ('a * ('a t) option)
val to_list : 'a t -> 'a list
end
We can then implement the module type in the following way:
module NonEmpty = struct
type 'a t = 'a list
(* INVARIANT: xs has length > 0 *)
let well_formed xs =
match xs with
| [] -> false
| _ :: _ -> true
let mk x = [x]
let cons x xs = x :: xs
let uncons xs =
match xs with
| [] -> failwith "UNREACHABLE"
| [x] -> (x, None)
| x :: xs' -> (x, Some xs')
let to_list xs = xs
end
Q1.1
For each of the functions in NonEmpty that (may) return a value of type 'a t, explain why the invariant well_formed must hold of the output, given that well_formed holds of the inputs.
Additionally, for uncons, explain why we should never throw the exception failwith "UNREACHABLE".
(*
Q1.1: Explain why the invariant is maintained for each of the following
functions, and UNREACHABLE should never be reached:
- mk: TODO
- cons: TODO
- uncons: TODO
*)
Q1.1: Grading Criteria
If you have valid explanations for each of the three functions.
Q1.2
For each of the following possible extensions to the NonEmptySig API, explain why it can or cannot be implemented in NonEmpty while maintaining the invariant. If it cannot be implemented while maintaining the invariant, explain how it could be adapted to a version that can maintain the invariant (for example, by returning an option and failing in some cases).
module type NonEmptySig = sig
...
(* Map the elements of the nonempty list by the function. *)
val map : 'a t -> ('a -> 'b) -> 'b t
(* Filter the nonempty list by the function (removing the element if the
function returns false on that element.) *)
val filter : 'a t -> ('a -> bool) -> 'a t
end
(* Q1.2:
- map: TODO
- filter: TODO
*)
Q1.2: Grading Criteria
For each function:
- Whether you have correctly identified if it can or cannot preserve the invariant as-is; and
- If it cannot preserve the invariant, how it could be changed to make the invariant preserved.
Q2: A type for balanced trees
Now, we will implement a type for balanced binary trees: one where the depth of any path in the tree is the same length as any other depth.
We will do so by first defining a raw tree:
We call it a raw tree since it allows the user to write non-balanced trees (such asNode (Leaf 3, Node (Leaf 4, Leaf 5))), which has a depth of 0 on the left subtree and 1 on the right subtree.)
Below is our module type defining an API for balanced trees:
module type BalancedTreeSig = sig
(* The type for balanced trees. *)
type 'a t
(* Is the balanced tree well-formed? *)
val well_formed : 'a t -> bool
(* Build the balanced tree consisting of a single leaf. *)
val leaf : 'a -> 'a t
(* Build the balanced tree consisting of a node (or returning None if it would result in a non-balanced tree.) *)
val node : 'a t -> 'a t -> 'a t option
(* Return the underlying raw_tree. *)
val to_tree : 'a t -> 'a raw_tree
end
In this question, you will implement a module for balanced trees:
module BalancedTree = struct
type 'a t = unit (* TODO: Replace with your chosen type! *)
let well_formed _ = failwith "TODO"
let leaf _ = failwith "TODO"
let node _ _ = failwith "TODO"
let to_tree _ = failwith "TODO"
end
You may choose any internal representation for balanced trees. (Since you need to implement to_tree, storing at least the underlying raw_tree will be useful.)
Then, implement well_formed, leaf, node, and to_tree.
For well_formed, you should ensure that any data stored within 'a t are valid: e.g., if it holds a raw_tree, that raw_tree should actually be balanced, and any other data that 'a t holds should also be well formed.
For node, node t1 t2 should return Some if and only if t1 and t2 are themselves well-balanced and have the same (constant) depth on all paths. If t1 and t2 have different depths, you should return None.
Hint
It will be useful to store more information than just the raw_tree in 'a t; perhaps something relating to the depth.
Q2: Grading Criteria
Whether you have:
- correctly implemented the four functions;
- correctly implemented
well_formedso that if it returns true, then the underlying tree is balanced; and - successfully preserved the
well_formedinvariant forleafandnode.