6.11. Exercises#
Solutions to most exercises are available. Fall 2022 is the first public release of these solutions. Though they have been available to Cornell students for a few years, it is inevitable that wider circulation will reveal improvements that could be made. We are happy to add or correct solutions. Please make contributions through GitHub.
Exercise: spec game [★★★]
Pair up with another programmer and play the specification game with them. Take turns being the specifier and the devious programmer. Here are some suggested functions you could use:
num_vowels : string -> int
is_sorted : 'a list -> bool
sort : 'a list -> 'a list
max : 'a list -> 'a
is_prime : int -> bool
is_palindrome : string -> bool
second_largest : int list -> int
depth : 'a tree -> int
Exercise: poly spec [★★★]
Let’s create a data abstraction for single-variable integer polynomials of the form
Let’s assume that the polynomials are dense, meaning that they contain very few coefficients that are zero. Here is an incomplete interface for polynomials:
(** [Poly] represents immutable polynomials with integer coefficients. *)
module type Poly = sig
(** [t] is the type of polynomials. *)
type t
(** [eval x p] is [p] evaluated at [x]. Example: if [p] represents
$3x^3 + x^2 + x$, then [eval 10 p] is [3110]. *)
val eval : int -> t -> int
end
Finish the design of Poly
by adding more operations to the interface. Consider
what operations would be useful to a client of the abstraction:
How would they create polynomials?
How would they combine polynomials to get new polynomials?
How would they query a polynomial to find out what it represents?
Write specification comments for the operations that you invent. Keep in mind the spec game as you write them: could a devious programmer subvert your intentions?
Exercise: poly impl [★★★]
Implement your specification of Poly
. As part of your implementation, you will
need to choose a representation type t
. Hint: recalling that our polynomials
are dense might guide you in choosing a representation type that makes for an
easier implementation.
Exercise: interval arithmetic [★★★★]
Specify and implement a data abstraction for interval arithmetic.
Be sure to include the abstraction function, representation invariant, and
rep_ok
. Also implement a to_string
function and a format
that can be
installed in the top level with #install_printer
.
Exercise: function maps [★★★★]
Implement a map (aka dictionary) data structure with abstract type ('k, 'v) t
.
As the representation type, use 'k -> 'v
. That is, a map is represented as an
OCaml function from keys to values. Document the AF. You do not need an RI. Your
solution will make heavy use of higher-order functions. Provide at least these
values and operations: empty
, mem
, find
, add
, remove
.
Exercise: set black box [★★★]
Go back to the implementation of sets with lists in the previous chapter.
Based on the specification comments of Set
, write an OUnit test suite
for ListSet
that does black-box testing of all its operations.
Exercise: set glass box [★★★]
Achieve as close to 100% code coverage with Bisect as you can for ListSet
and UniqListSet
.
Exercise: random lists [★★★]
Use QCheck.Gen.generate1
to generate a list whose length is between 5 and 10,
and whose elements are integers between 0 and 100. Then use
QCheck.Gen.generate
to generate a 3-element list, each element of which is a
list of the kind you just created with generate1
.
Then use QCheck.make
to create an arbitrary that represents a list whose
length is between 5 and 10, and whose elements are integers between 0 and 100.
The type of your arbitrary should be int list QCheck.arbitrary
.
Finally, create and run a QCheck test that checks whether at least one element
of an arbitrary list (of 5 to 10 elements, each between 0 and 100) is even.
You’ll need to “upgrade” the is_even
property to work on a list of integers
rather than a single integer.
Each time you run the test, recall that it will generate 100 lists and check the property of them. If you run the test many times, you’ll likely see some successes and some failures.
Exercise: qcheck odd divisor [★★★]
Here is a buggy function:
(** [odd_divisor x] is an odd divisor of [x].
Requires: [x >= 0]. *)
let odd_divisor x =
if x < 3 then 1 else
let rec search y =
if y >= x then y (* exceeded upper bound *)
else if x mod y = 0 then y (* found a divisor! *)
else search (y + 2) (* skip evens *)
in search 3
Write a QCheck test to determine whether the output of that function (on a positive integer, per its precondition; hint: there is an arbitrary that generates positive integers) is both odd and is a divisor of the input. You will discover that there is a bug in the function. What is the smallest integer that triggers that bug?
Exercise: qcheck avg [★★★★]
Here is a buggy function:
(** [avg [x1; ...; xn]] is [(x1 + ... + xn) / n].
Requires: the input list is not empty. *)
let avg lst =
let rec loop (s, n) = function
| [] -> (s, n)
| [ h ] -> (s + h, n + 1)
| h1 :: h2 :: t -> if h1 = h2 then loop (s + h1, n + 1) t
else loop (s + h1 + h2, n + 2) t
in
let (s, n) = loop (0, 0) lst
in float_of_int s /. float_of_int n
Write a QCheck test that detects the bug. For the property that you check,
construct your own reference implementation of average—that is,
a less optimized version of avg
that is obviously correct.
Exercise: exp [★★]
Prove that exp x (m + n) = exp x m * exp x n
, where
let rec exp x n =
if n = 0 then 1 else x * exp x (n - 1)
Proceed by induction on m
.
Exercise: fibi [★★★]
Prove that forall n >= 1, fib n = fibi n (0, 1)
, where
let rec fib n =
if n = 1 then 1
else if n = 2 then 1
else fib (n - 2) + fib (n - 1)
let rec fibi n (prev, curr) =
if n = 1 then curr
else fibi (n - 1) (curr, prev + curr)
Proceed by induction on n
, rather than trying to apply the theorem about
converting recursion into iteration.
Exercise: expsq [★★★]
Prove that expsq x n = exp x n
, where
let rec expsq x n =
if n = 0 then 1
else if n = 1 then x
else (if n mod 2 = 0 then 1 else x) * expsq (x * x) (n / 2)
Proceed by strong induction on n
. Function expsq
implements
exponentiation by repeated squaring, which results in more efficient
computation than exp
.
Exercise: expsq simplified [★★★]
Redo the preceding exercise, but with this simplified version of the function. The simplified version requires less code, but requires an additional recursive call.
let rec expsq' x n =
if n = 0 then 1
else (if n mod 2 = 0 then 1 else x) * expsq' (x * x) (n / 2)
Exercise: mult [★★]
Prove that forall n, mult n Z = Z
by induction on n
, where:
let rec mult a b =
match a with
| Z -> Z
| S k -> plus b (mult k b)
Exercise: append nil [★★]
Prove that forall lst, lst @ [] = lst
by induction on lst
.
Exercise: rev dist append [★★★]
Prove that reverse distributes over append, i.e., that
forall lst1 lst2, rev (lst1 @ lst2) = rev lst2 @ rev lst1
, where:
let rec rev = function
| [] -> []
| h :: t -> rev t @ [h]
(That is, of course, an inefficient implementation of rev
.) You will need to
choose which list to induct over. You will need the previous exercise as a
lemma, as well as the associativity of append
, which was proved in the notes
above.
Exercise: rev involutive [★★★]
Prove that reverse is an involution, i.e., that
forall lst, rev (rev lst) = lst
. Proceed by induction on lst
. You will need
the previous exercise as a lemma.
Exercise: reflect size [★★★]
Prove that forall t, size (reflect t) = size t
by induction on t
, where:
let rec size = function
| Leaf -> 0
| Node (l, v, r) -> 1 + size l + size r
Exercise: fold theorem 2 [★★★★]
We proved that fold_left
and fold_right
yield the same results if their
function argument is associative and commutative. But that doesn’t explain why
these two implementations of concat
yield the same results, because ( ^ )
is
not commutative:
let concat_l lst = List.fold_left ( ^ ) "" lst
let concat_r lst = List.fold_right ( ^ ) lst ""
Formulate and prove a new theorem about when fold_left
and fold_right
yield
the same results, under the relaxed assumption that their function argument is
associative but not necessarily commutative. Hint: make a new assumption about
the initial value of the accumulator.
Exercise: propositions [★★★★]
In propositional logic, we have atomic propositions, negation, conjunction,
disjunction, and implication. For example, raining /\ snowing /\ cold
is a
proposition stating that it is simultaneously raining and snowing and cold (a
weather condition known as Ithacating).
Define an OCaml type to represent propositions. Then state the induction principle for that type.
Exercise: list spec [★★★]
Design an OCaml interface for lists that has nil
, cons
, append
, and
length
operations. Design the equational specification. Hint: the equations
will look strikingly like the OCaml implementations of @
and List.length
.
Exercise: bag spec [★★★★]
A bag or multiset is like a blend of a list and a set: like a set, order does not matter; like a list, elements may occur more than once. The number of times an element occurs is its multiplicity. An element that does not occur in the bag has multiplicity 0. Here is an OCaml signature for bags:
module type Bag = sig
type 'a t
val empty : 'a t
val is_empty : 'a t -> bool
val insert : 'a -> 'a t -> 'a t
val mult : 'a -> 'a t -> int
val remove : 'a -> 'a t -> 'a t
end
Categorize the operations in the Bag
interface as generators, manipulators, or
queries. Then design an equational specification for bags. For the remove
operation, your specification should cause at most one occurrence of an element
to be removed. That is, the multiplicity of that value should decrease by at
most one.