PoP assignment 2: variance three ways and a pizza module in OCaml
My second hand-in for Programmering og problemløsning at KU — one variance function written with a loop, with recursion and with higher-order functions, input partitioning for tests, and a pizza module as an abstract data type.
This post was AI-generated from my hand-in — the report and the OCaml source files.
Course: Programmering og problemløsning (PoP) 2026, University of Copenhagen. The course is graded pass/fail on four assignments, each with a submission and a mandatory resubmission. This is the first submission of assignment 2, handed in on 23 October; the resubmission is due 6 November.
The assignment has two parts: a variance function implemented with each of the three kinds of iteration from class, and a pizza module defined as an abstract data type, both tested with input partitioning.
1. Variance
Specification
The definition comes from the W3Schools page on variance: find the mean, take each element's difference from the mean, square it, and take the mean of the squares. That is the population variance:
My specification:
(* Returns variance given a list of floats *)
calculate_variance : float list -> float
find the mean
calculate the difference per element with the mean
square the difference
find the mean of the squared differences
Return 0, given an empty list
The empty list is the one decision that needs making. The mean divides by the length, and the length of [] is 0, so I decided to return 0.0 rather than fail.
Three implementations
Week 5 named three ways to iterate: loops, recursion and higher-order functions. All three versions are in variance.ml.
Loop. One function. Two while loops walk the list with a ref to "what's left": the first sums the elements to get the mean, the second sums the squared differences.
let calculate_variance_loop (elements : float list) : float =
if List.length elements = 0 then 0.0 else
let sum = ref 0.0 in
let remaining = ref elements in
while !remaining <> [] do
let x = List.hd !remaining in
sum := !sum +. x;
remaining := List.tl !remaining
done;
let mean = !sum /. float_of_int (List.length elements) in
(* second loop: same walk, summing (x - mean)^2 *)
...
Recursion. Broken into three small functions, each with a case for [] and a case for x :: rest:
let rec sum (elements : float list) : float =
match elements with
| [] -> 0.0
| x :: rest -> x +. sum rest
let rec length (elements : float list) : int =
match elements with
| [] -> 0
| _ :: rest -> 1 + length rest
let rec sum_squared_diff (elements : float list) (mean : float) : float =
match elements with
| [] -> 0.0
| x :: rest -> (x -. mean) *. (x -. mean) +. sum_squared_diff rest mean
let calculate_variance_rec (elements : float list) : float =
if elements = [] then 0.0 else
let float_length = float_of_int (length elements) in
let mean = sum elements /. float_length in
sum_squared_diff elements mean /. float_length
Higher-order functions. List.fold_left for the sums and List.map for the squared differences:
let calculate_variance_hof (elements : float list) : float =
if elements = [] then 0.0 else
let float_length = float_of_int (List.length elements) in
let sum = List.fold_left (fun acc e -> acc +. e) 0.0 elements in
let mean = sum /. float_length in
let squared_diffs = List.map (fun e -> (e -. mean) *. (e -. mean)) elements in
List.fold_left (fun acc e -> acc +. e) 0.0 squared_diffs /. float_length
The three get progressively shorter. The loop spells out how to walk the list; the fold version only says what to combine, which is the imperative-versus-declarative contrast from week 5.
Tests by input partitioning
Input partitioning means splitting the valid inputs into disjoint classes and picking a representative or two from each. I expected the typical input to be a list of positive floats, so the classes are the inputs at the edges of that:
Class · Input · Expected · Why Empty list · [] · 0.0 · the mean would divide by 0 One element · [5.0] · 0.0 · nothing to vary Negative elements · [-5.0; -2.0] · 2.25 · variance stays non-negative Same elements · [5.0; 5.0; 5.0] · 0.0 · every difference is 0
For the negative class, the mean of and is , both differences are , and the variance is .
The suite follows the test_bubblesort.ml style from class: a list of (class name, [(input, expected)]) pairs, and a runtest that takes the function under test as an argument, so the same suite runs against all three implementations:
runtest calculate_variance_loop;;
runtest calculate_variance_rec;;
runtest calculate_variance_hof;;
All three pass 4 of 4. On the seven dog heights from the W3Schools example, all three give , matching the page.
Bugs found
None when the tests ran, but only because writing the tests sent me back to the code first. The empty-list check was in my specification and not in my first implementations. Without it, [] gives 0.0 /. 0.0, which in OCaml is not an exception but nan, and the function returns it without complaint.
Pitfall: float division by zero doesn't crash in OCaml. 0.0 /. 0.0 is nan and 1.0 /. 0.0 is infinity, so a missing empty-list case shows up as a strange result, not an error.
2. A pizza module
The task: represent what a customer decides when ordering a pizza, plus its price, as an abstract data type, a type that can only be used through the operations in its interface.
The interface
pizza.mli exposes the choices as variants and keeps pizza itself abstract:
(* price depends on: size and crust, 10 for small, 15 for medium,
20 for large, and filled crust costs an extra 2$ *)
type size = Small | Medium | Large
type toppings = Ham | Pepperoni | Pineapple
type crust = Filled | Regular
type pizza
(* lets the customer select the size, topping and crust *)
val create_pizza : size -> toppings -> crust -> pizza
(* the price, which depends on the selected crust and size *)
val generate_price : pizza -> int
Because type pizza has no = ... in the interface, code outside the module can't build or take apart a pizza directly. It has to go through create_pizza.
The implementation
Inside pizza.ml, a pizza is just a triple. The price ignores the topping:
type pizza = size * toppings * crust
let create_pizza (selected_size : size) (topping : toppings) (selected_crust : crust) : pizza =
(selected_size, topping, selected_crust)
let generate_price (pizza : pizza) : int =
let (s, _, c) = pizza in
let size_price = match s with
| Small -> 10
| Medium -> 15
| Large -> 20
in
let crust_price = match c with
| Regular -> 0
| Filled -> 2
in
size_price + crust_price
Tests
My first idea for partitions was the incomplete orders: a pizza with no size, no crust or no topping. The types rule those out: create_pizza takes exactly one of each, so a pizza with no size doesn't compile. That makes the valid inputs a finite set, combinations, and I tested every one of them:
· Regular · Filled Small · 10 · 12 Medium · 15 · 17 Large · 20 · 22
The same table holds for each of the three toppings, which is the point: the topping must not change the price.
The tests are compiled together with the module, as in week 6:
ocamlc pizza.mli pizza.ml test_pizza.ml -o test_pizza
./test_pizza
and pass 18 of 18.
The hand-in
