Tutorial for Module 2
This is the tutorial video for Module 2. We will work through five
small programs that exercise everything in the module:
literals, let bindings,
type inference,
operators, and
if-expressions. After the worked
problems, we will dwell on the three type errors you will see most
often in your first programs, and close with an activity for you
to try.
The point of the tutorial is to type code and meet the type errors when they show up. Every cell is editable. Make deliberate mistakes; see what the compiler says; fix them. The five-minute frustration of "why won't this compile" is the fastest path to fluency.
Problem 1: classify a response time
A function that returns a label for an HTTP response time in milliseconds. The classification: under 50ms is "instant", under 200ms is "fast", under 1000ms is "noticeable", anything else is "slow".
Result for 180.0: string = "fast". Try the boundaries:
response_class 50.0 returns "fast" (because < is strict; 50
is not less than 50); response_class 200.0 returns
"noticeable". The choice of < vs <= at thresholds is a
judgement call. Both are right; this version treats 50 ms as
"fast" and 200 ms as "noticeable". If you would rather it be the
other way (treat 50 ms as "instant"), swap < for <=. The
point is to be deliberate.
This is also a good example of a function that has type float -> string: the operator drives inference. The comparisons are
against float literals (50.0, 200.0, etc.), so ms is
float; the branches return string literals, so the body has
type string; the function is float -> string.
Problem 2: leap year
A year is a leap year if it is divisible by 4, unless divisible by 100, unless again divisible by 400. So 2000 is a leap year (divisible by 400), 1900 is not (divisible by 100 but not by 400), 2024 is (divisible by 4, not by 100), 2025 is not (not divisible by 4).
Expected: true, false, false, true. The parentheses around the
first && are not strictly needed (&& binds tighter than ||,
so the parse is the same either way), but they make the rule
readable. The expression "either (divisible by 4 and not by 100)
or (divisible by 400)" reads off the code with the parens; without
them you have to mentally insert them. Explicit parens cost
nothing at runtime; spend them.
This is a useful place to notice that mod produces an int,
which we then compare with =. The comparisons are all
int = int, so they all type-check; the && and || glue them
into one bool-typed expression.
Problem 3: shipping cost label
A two-function problem: a shipping table that computes cost from a package's weight (kg), and a labeller that categorises the cost as "cheap", "standard", or "premium". Here is the solution:
Result for 2.5: string = "standard" (weight 2.5 falls in the
< 5.0 band, so cost = 10.0, which is < 25.0, so the label
is "standard"). The pattern let cost = shipping_cost weight in if cost < ... else ... is idiomatic:
when you need to inspect the same value at several thresholds,
name it once and compare repeatedly. Without the let, you would
compute shipping_cost weight three times in the if-chain (once
for each threshold), which is wasteful and clutters the code.
The function shipping_label is built by composing two smaller
functions, shipping_cost and an if-chain. This is the rhythm
of functional programming: small, focused functions, combined
into larger behaviours. Module 6
will give us tools to make this composition explicit; here it is
just let + function call.
Problem 4: clamp
Constrain a value to a given range. If the value is below the lower bound, return the lower bound; if above the upper bound, return the upper bound; otherwise return the value as-is.
Results: 7, 0, 10. The function's type is int -> int -> int -> int. Note the argument order: lo, hi, x. There is no one
right argument order; this one mirrors the conceptual reading
("clamp into the range lo..hi, the value x"). Another defensible
order is x lo hi; both are fine, just be consistent.
The parenthesisation (-3) is the
unary-minus pitfall from the operators lecture
(without parens it would parse as subtraction). Worth remembering.
Problem 5: tying it together
A small utility function for "divide safely":
Results: 26.25 (which is (100 + 5) / 4) and 0.0. The second
call would have been a divide-by-zero in a /. b, but safe_divide
intercepts it and returns 0.0 instead.
A short aside: replacing a bad case with a "sentinel" value
(returning 0.0 for divide-by-zero) is a design decision, and
not always the right one. The sentinel can hide real bugs: if your
caller didn't notice that you returned 0.0, they might
incorporate it into a subsequent computation and silently produce
nonsense. The alternatives are:
- Raise an exception (we cover exceptions in Module 7) so the caller has to handle the case explicitly.
- Return an
optionorresulttype (Module 4) that encodes "this might be a valid number, or it might be 'no answer'". Forces the caller to check.
For a tutorial example, the sentinel is fine. In production code, either of the two alternatives is usually better. Mention this to set up Modules 4 and 7.
Reading type errors
Type errors are noisy at first. The cure is repetition: write some code, read the message, fix, repeat. One error worth a fresh slide here; two more were covered earlier in the module.
The int/float operator mix-up: you wrote * when you meant *..
The compiler points at the float literal as the offender, says
it expected an int (because * is integer multiplication), and
tells you the actual type is float. The fix: change the
operator to *..
The trick to reading the error: the operator drives the expected
type. If you see "expected int", look for an int operator
nearby; that's where the constraint came from.
Two more error shapes you have already seen elsewhere in the module are worth re-skimming when you hit them:
- The operators lecture, Pitfall 2:
"value: " ^ 5fails because OCaml does not silently coerceinttostring. Convert withstring_of_intor usePrintf.sprintf. - The
iflecture, mismatched branches:if ... then "positive" else 0fails because the two branches must share a type. Decide which type you want and rewrite the other branch.
Together these three shapes (operator mismatch, missing conversion, mismatched branches) account for the bulk of first-week type errors. After enough repetition the muscle memory takes over.
Activity
Try this one yourself before reading on.
Write sign : int -> int that returns -1 for negative inputs,
0 for zero, and 1 for positive inputs.
Now write the float version: sign_f : float -> float
returning -1.0, 0.0, 1.0.
Show reference solution
Compare the two versions. The logic (negative? zero? positive?)
is identical. What changed is the literals: 0 becomes 0.0,
-1 becomes -1.0, etc. OCaml made you write out the type choice;
the algorithm itself didn't change. This is the cost of the no-implicit-conversion
rule. The benefit is that anyone reading either function knows
unambiguously what types are involved.
A small philosophical aside, since the think about this prompt
invites it. Could you replace the three-way if in sign with
arithmetic? Almost; one if survives, to guard the zero case:
This works: x / abs x is 1 for positive and -1 for negative,
and we handle the 0 case separately to avoid division by zero.
It is more compact than the three-branch if, but arguably
less clear: a reader has to think to convince themselves that
the formula gives the right answer. The three-branch version
reads like the specification.
This is a general theme: cleverness and clarity are different virtues, and clarity usually wins. We will see this again with recursion versus higher-order functions (Module 6).
What you should be able to do now
By the end of Module 2 you should be comfortable doing the following without checking references:
If any of these still feel shaky, the right move is to go back to the relevant lecture and re-attempt the quizzes. Module 3 will assume Module 2 is solid: we will start treating functions as values you can pass around, store, and return from other functions. That's where OCaml starts to feel like a genuinely different language from C or Python, and you'll want the expression-level mechanics from Module 2 to be automatic.
Reading
- Cornell CS3110, Basics chapter: a denser version of the same material if anything felt thin: https://cs3110.github.io/textbook/chapters/basics/index.html
- Real World OCaml, A Guided Tour: another angle: https://dev.realworldocaml.org/guided-tour.html
Sources
This lecture's prose, worked examples, and quizzes are original to
this course. Materials referenced during preparation are listed in
the Reading section above; Cornell CS3110 and Real World OCaml
are CC BY-NC-ND-licensed and have not been derivatively reused.
See LICENSES.md
at the repository root for the full source posture.