Functions, Types & Choices
Values are the nouns of your game. Today you write the verbs — functions that deal damage, size up dungeons and pick winners — and you meet the compiler's crystal ball: type inference.
You will be able to…
- Define and call functions — one argument or several
- Read arrow types like
int -> int -> intand predict them before Run - Pin types down with annotations like
(hp : int) - Use
if/elseas an expression that produces a value - Write your first recursive function — one that calls itself
Teach the machine a new trick
In lesson 1.1 you named values: let lives = 3. A function
is the same let, plus a parameter before the
= — a blank that gets filled in later. Whatever the body
evaluates to is what the function returns. No return keyword
exists, or is needed:
Calling is just as minimal: double 25. A space, no
parentheses, no commas. Here's the toplevel's side of the
conversation:
Look at that answer line: int -> int. Read the arrow as
a promise — "give me an int, I'll give you back an
int". The <fun> just means "the value
is code" — the toplevel won't print your recipe back at you.
And here's the eerie part: you never said n was
an int. The compiler deduced it. * only works on
ints, so n must be an int, so the result is one too. That
detective work is called type inference, and it runs on everything
you write, all the time, for free.
A "Double XP Weekend" begins! Write a function double
that returns twice its argument.
*.return — the last expression IS the result.The crystal ball also guards the gate. Try to smuggle a string into
double — run this and read the rejection letter slowly:
"This expression has type string but an expression was
expected of type int." Decode it: this expression is
"hi"; expected is what double's promise
demands. Some languages would happily compute 2 * "hi" at
3 a.m. in a player's browser and crash the boss fight. OCaml refuses at
compile time, while the bug is still cheap.
More ingredients, and pinning types down
Need two inputs? Stack the parameters:
int -> int -> int: one arrow per argument,
and the final type is the result. Count arrows, know the shape — this one
skill lets you read any OCaml library's documentation.
Inference usually gets it right on its own, but you can also
annotate — state a type out loud. Wrap a parameter as
(name : type), and put : type before the
= for the result:
Annotations don't change what the function does. They document intent, and they give the compiler a place to point when something disagrees — error messages land closer to the real culprit.
Every dungeon room in our future games is a grid of tiles. Write
area, taking a width and a height (both ints) and
returning how many tiles the room holds. For style points, annotate
the parameters and the result.
let area w h = …let area (w : int) (h : int) : int = w * h.Choices: if/else is an expression
Comparisons produce bools: = (equal),
<> (not equal), <, <=,
>, >=. Combine facts with
&& (and), || (or), flip one with
not. And here is the twist that makes OCaml OCaml: an
if/else isn't a statement that does something — it's
an expression that is something. It produces a value, so you can
bind it, print it, or feed it to a function:
Because the whole if must have one type, both branches
must agree: if hp > 0 then "fighting" else 0 is refused —
a string from one door, an int from the other,
and the compiler won't guess which you meant. Same reasoning explains why
else is required here: without it, what would
status be when hp is zero?
Often you don't even need if. A comparison already
is a bool:
Read is_even inside-out: n mod 2 is the
remainder after dividing by two; = 0 asks whether it's zero;
that question's answer — a bool — is the result. Writing
if n mod 2 = 0 then true else false works, but it's asking a
yes/no question and then repeating the answer back to yourself.
Three heroes walk in with power levels a,
b, c. Write max3 returning the
highest of the three. Chained if/else is the intended
path — though the stdlib's max is legal too.
if … then … else if … then … else …a it must beat both rivals: a >= b && a >= c.if a >= b && a >= c then a else if b >= c then b else c — or cheat gloriously with max a (max b c).Functions that call themselves
One more superpower and we'll call it a day. A function may use itself
in its own body — if you warn the compiler with rec:
Trace it like a stack of quest scrolls: shout 3 is
"Ha" ^ shout 2, which is "Ha" ^ "Ha" ^ shout 1…
until shout 0 answers "" and the whole tower
collapses into "HaHaHa". Every recursive function obeys two
sacred rules: a base case that answers without calling itself, and
recursive calls that step toward that base case. Break either rule
and the function falls forever.
A combo chain pays 1 coin on the first hit, 2 on the second, 3 on
the third… Write sum_to so that sum_to n
returns 1 + 2 + … + n (and 0 for a whiffed
combo, n = 0). Legend says Gauss summed to 100 instantly
with a formula — your function gets to earn it the honest, recursive
way.
n pays nothing.else adds n to a smaller sum: n + sum_to (n - 1) — parentheses around n - 1!Deeper: how far can inference see?
Across your whole program. And when nothing constrains a parameter,
OCaml doesn't guess — it generalises: let id x = x gets the
type 'a -> 'a, where 'a (say "alpha") means
any type you like, but the same one in and out. One function,
infinitely many uses, still completely type-safe. This is why stdlib
functions like max work on ints, floats and strings alike.
You'll wield 'a yourself when lists arrive in Phase 2.
Checkpoint quiz
Loot collected
let f x = …defines;f 42calls — spaces, not parentheses.- Arrow types: one
->per argument, result at the end. Inference figures them out; annotations(x : int)pin them down. if/elseis an expression; both branches must share one type.- Comparisons (
=,<>,<=…) makebools;&&/||combine them. let recunlocks recursion: base case + smaller steps, always.
Next: we leave the browser for an hour and forge the real toolchain on your machine — then come back armed.