PoP assignment 1: bills of materials and study groups in OCaml
My first hand-in for Programmering og problemløsning at KU — record and variant types, recursion over lists, and two ways to make illegal study groups impossible.
This post was AI-generated from my hand-in — the LaTeX 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 1; the resubmission is due 9 October.
The assignment has three parts: a bill of materials (BOM) with a cost and a difference function, splitting students into study groups, and a short reflection on types and recursion.
1. Bill of materials
Types
A component has an id, a name and a price. A BOM is a list of components, each paired with the quantity needed.
type component = { id : int; name : string; price : float }
type component_and_quantity = { comp : component; quantity : int }
type bom = component_and_quantity list
The price is a float because there can be cents. The quantity is an int because you can't have half a door.
Cost
cost : bom -> float sums price times quantity. A list is either empty or a first element followed by the rest, so the function has exactly two cases:
let rec cost (b : bom) : float =
match b with
| [] -> 0.0
| caq :: rest -> caq.comp.price *. float_of_int caq.quantity +. cost rest
A car — one frame at 5000, four wheels at 100, four screws at 0.5 — costs 5402.0.
OCaml keeps integer and float arithmetic apart: *. and +. are the float operators, and the int quantity has to be converted with float_of_int before it can be multiplied by a price.
Difference
difference b1 b2 gives, for every component in either BOM, the quantity in b1 minus the quantity in b2. Components with difference 0 are left out, and a negative quantity means b2 needs more of that component.
A helper looks up a component's quantity, with 0 meaning "not there":
let rec quantity (id : int) (b : bom) : int =
match b with
| [] -> 0
| caq :: rest -> if caq.comp.id = id then caq.quantity else quantity id rest
Then the work splits in two: components that appear in b1, and components that appear only in b2. @ joins the two results.
let difference (b1 : bom) (b2 : bom) : bom =
let rec from_b1 (b : bom) : bom =
match b with
| [] -> []
| caq :: rest ->
let diff = caq.quantity - quantity caq.comp.id b2 in
if diff = 0 then from_b1 rest
else { comp = caq.comp; quantity = diff } :: from_b1 rest
in
let rec only_b2 (b : bom) : bom =
match b with
| [] -> []
| caq :: rest ->
if quantity caq.comp.id b1 = 0
then { comp = caq.comp; quantity = - caq.quantity } :: only_b2 rest
else only_b2 rest
in
from_b1 b1 @ only_b2 b2
Tests
The tests are plain asserts that run when the file is executed:
let () =
assert (difference car car = []);
assert (difference car [] = car);
assert (difference [] [ { comp = nail; quantity = 2 } ]
= [ { comp = nail; quantity = -2 } ]);
assert (cost (difference car computer) = cost car -. cost computer);
print_endline "bom.ml: all tests passed"
The last one ties the two functions together: the cost of the difference has to equal the difference of the costs. It checks the signs in difference without spelling out the expected list by hand.
2. Study groups
Students belong to one of four study lines. The task is to split each line into groups of at most three.
type study_line = ComputerScience | MachineLearning | Economics | Math
type student = { id : int; name : string; line : study_line }
Two representations of a group
(* 1: one constructor per group size *)
type group = One of student
| Two of student * student
| Three of student * student * student
(* 2: a plain list, with the invariant "1 to 3 students" *)
type group_list = student list
The first one makes illegal groups impossible to build. There is no way to write an empty group or a group of four, and the compiler warns when a match forgets one of the sizes. The second is simpler and works with every list function, but nothing stops a list of ten students — the invariant lives in my head, not in the type. I used the first.
Grouping
Two helpers do the work. students_of keeps the students on one study line:
let rec students_of (l : study_line) (ss : student list) : student list =
match ss with
| [] -> []
| s :: rest -> if s.line = l then s :: students_of l rest else students_of l rest
make_groups takes three students at a time instead of one, so it needs a case for 0, 1 or 2 students left over:
let rec make_groups (ss : student list) : group list =
match ss with
| [] -> []
| [ a ] -> [ One a ]
| [ a; b ] -> [ Two (a, b) ]
| a :: b :: c :: rest -> Three (a, b, c) :: make_groups rest
groups returns one list of groups per study line, as a 4-tuple in the order of the type definition:
let groups (ss : student list) : group list * group list * group list * group list =
( make_groups (students_of ComputerScience ss),
make_groups (students_of MachineLearning ss),
make_groups (students_of Economics ss),
make_groups (students_of Math ss) )
With four computer science students, two machine learning, one economics and no math students, the result is ([Three (s1, s3, s4); One s7], [Two (s2, s5)], [One s6], []). Gustav ends up in a group on his own.
3. Concepts
Types. A type is a set of values together with the operations allowed on them. OCaml checks types before the program runs, so 1 + 2.0 is rejected. The assignment defines three kinds of new types: records (component, student), which group named fields into one value; type abbreviations (bom), which give an existing type a meaningful name; and variants (study_line, group), which list the alternatives a value can be.
Recursion. A recursive function calls itself on a smaller input until it reaches a base case — here almost always the empty list. Every function in the assignment has the same shape: one case for [], one for first :: rest.
What was hard
Mostly the syntax. I write Python, TypeScript and C# for work, and I have written recursive functions before, but I usually reach for for and while loops. After a while the pattern-matching style started to feel cleaner than the loops: there is less bookkeeping.
The study_line variant felt familiar — it is close to an enum in C#. The line that took longest was a :: b :: c :: rest -> in make_groups, which matches three students at once so a group never gets a fourth.
I used Claude Code for the hand-in and logged every prompt, as the course requires. It wrote the test cases, helped with the syntax of the a :: b :: c :: rest pattern (the logic was mine), produced the LaTeX skeleton from the course's Overleaf template and the Makefile, and pulled the code snippets into the report.