ITT8060 Advanced Programming · Autumn 2026
Tallinn University of Technology
Full prose version with runnable examples: notes.html
ITT8060Last week: a lightning tour — let, functions, tuples, if,
recursion, match.
This week: what do they mean?
Typing rules and evaluation rules.
ITT8060f 0 = 1
f (n + 1) = (n + 1) * f n
f 3 // n is 2
~> 3 * f 2 // rhs is (2 + 1) * f 2
~> 3 * (2 * f 1)
~> 3 * (2 * (1 * f 0))
~> 3 * (2 * (1 * 1))
~> 6
Read the equations defining the function top-to-bottom, left-to-right.
An expression matching a left-hand side reduces to the right-hand side (reading top-to-bottom).
e ~> e': expression e reduces to e'.
ITT8060\(e \leadsto e'\) — reduction: \(e\) reduces to \(e'\). In traces: ~>.
\(e[v/x]\) — substitution: \(e\) with the value \(v\) substituted for the variable \(x\).
\(e : t\) — typing: expression \(e\) is of type \(t\).
Rules have the following form where premises are above and conclusion is below the bar.
\[\dfrac{\text{premise}_1 \qquad \text{premise}_2}{\text{conclusion}}\]
Program text in teletype, metavariables in italics: \(\mathtt{if}\ b\ \mathtt{then}\ e_1\ \mathtt{else}\ e_2\).
ITT8060
ITT8060A functional program is an expression; evaluating (or running) it means reducing it.
f 3 is an expression. It is reducible: f 3 ~> 3 * f 2, which is again reducible.
Some expressions do not reduce any further: the values. f 0 is not a value; 1 is.
Evaluation terminates with a value.
Not every expression is sensible. What is f f? What is 3 3?
A type system excludes certain syntactically correct expressions.
ITT8060\(e : t\) — "expression \(e\) is of type \(t\)". For example 5 : int and 3 + 2 : int.
For an undesirable expression (such as f f) there is no t with e : t.
For every type, a designated set of expressions are its values
(including the literals: 0, true, "abc", 1.0, …).
Which of the following are values?
2 + 2 : int4 : int4 % 2 = 0 : bool4 % 2 : intsqrt 2.0 : float
ITT8060Preservation: reduction steps preserve the type.
\[\text{if } e : t \text{ and } e \leadsto e' \text{, then } e' : t.\]
An expression of type int cannot evaluate to a value of type string.
Progress: a well-typed expression is a value or can take a reduction step.
\[\text{if } e : t \text{, then } e \text{ is a value, or } e \leadsto e' \text{ for some } e'.\]
Evaluation does not get stuck (append s : string to a function f :
int -> int).
Well-typed programs don’t go wrong.
ITT8060
ITT80603.14 * 1.0 * 1.0 // radius 1.0
3.14 * 2.0 * 2.0 // radius 2.0
3.14 * 3.0 * 3.0 // radius 3.0
3.14 * r * r // any radius
We do not want a distinct program for every possible radius.
An expression may contain variables. We may think of a variable as a hole into which we plug values.
A variable may occur multiple times in an expression.
The occurrences of r in 3.14 * r * r are free: nothing in the
expression introduces the variable r.
ITT8060(3.14 * r * r)[2.0/r]
is 3.14 * 2.0 * 2.0
((3.14 * r * r)[2.0/r])[3.0/r]
is (3.14 * 2.0 * 2.0)[3.0/r]
is 3.14 * 2.0 * 2.0
\(e[v/x]\): substitute \(v\) for the free occurrences of \(x\) in \(e\).
Once substituted, the variable occurrence is gone.
Substitution is consistent: every free occurrence of the same variable gets the same value.
Different values for different occurrences? Use different variables:
3.14 * r * s.
ITT8060let expressions
ITT8060let-expressionslet x = e in e'
x is an identifier, e is the binding expression and e' is the body expression.
Local variable x with scope e'; its value is the result of evaluating e.
In e', occurrences of x are bound (no longer free) as the
let introduces x. (Says nothing about e.)
Substituting for x does not touch e' as those occurrences are bound (but not those in e):
\((\mathtt{let}\ x = e\ \mathtt{in}\ e')[v/x]\) is \(\mathtt{let}\ x = e[v/x]\ \mathtt{in}\ e'\)
ITT8060letBinding expression is a value: the let disappears.
\[\mathtt{let}\ x = v\ \mathtt{in}\ e' \;\leadsto\; e'[v/x] \qquad (v \text{ a value})\]
Binding expression is not yet a value: it takes a step.
\[\dfrac{e \leadsto e''}{\mathtt{let}\ x = e\ \mathtt{in}\ e' \;\leadsto\; \mathtt{let}\ x = e''\ \mathtt{in}\ e'}\]
e to v.v for x in e'.e'[v/x] to v'. The overall result is v'.
ITT8060let r = 1.0 + 1.0 in 3.14 * r * r
~> let r = 2.0 in 3.14 * r * r
~> (3.14 * r * r)[2.0/r]
~> 3.14 * 2.0 * 2.0
~> 12.56
let letExample =
let r = 1.0 + 1.0 in 3.14 * r * r
// val letExample: float = 12.56
On the left: reduction steps.
On the right: same expression in F# interpreter.
ITT8060let\[\dfrac{e : t \qquad e' : t' \ (\text{ assuming } x : t)}{(\mathtt{let}\ x = e\ \mathtt{in}\ e') : t'}\]
Note that e and x have the same type. Recall that the evaluation
rule substitutes the value of e for x.
In F#, the keyword in may be replaced by a newline. Then the scope
extends to the rest of the enclosing block.
ITT8060let needs a body> (let x = 1) + 1;;
(let x = 1) + 1;;
-^^^
error FS0588: The block following this
'let' is unfinished. Every code block is
an expression and must have a result.
let letInline = (let x = 1 in x + x) + 1
// val letInline: int = 3
Recall that the result of a let-expression is the result of the body e'.
If the body is missing, then the compiler will complain.
The addition of in x + x completes the let expression (with x + x as the body).
ITT8060let r = 1.0 in
r + let r = 2.0 in 3.14 * r * r
Every let x = … binds a new variable. Same name as one in scope? The
new one shadows the outer one in its scope.
Recall that we substitute into free occurrences of variables.
(r + let r = 2.0 in 3.14 * r * r)[1.0/r]
is 1.0 + let r = 2.0 in 3.14 * r * r
The body of the let (since it introduces r) is not affected.
The result of evaluating (r + let r = 2.0 in 3.14 * r * r)[1.0/r] is
13.56.
ITT8060let definitionsAt the top level, a let does not require a body expression (the rest
of the scope is the body).
let bad = 1 + pi // pi not in scope
let pi = 3.14
let good = 2 + pi // pi in scope
ITT8060
ITT8060if b then e1 else e2
b conditione1 the true branche2 the false branchlet ifInline = 1 + if true then 0 else 1
// val ifInline: int = 1
A conditionl can appear anywhere an expression is expected (unlike statements in imperative languages).
ITT8060ifTwo rules when condition is a value:
\[\mathtt{if}\ \mathtt{true}\ \mathtt{then}\ e_1\ \mathtt{else}\ e_2 \;\leadsto\; e_1 \qquad \mathtt{if}\ \mathtt{false}\ \mathtt{then}\ e_1\ \mathtt{else}\ e_2 \;\leadsto\; e_2\]
One rule when condition is not yet a value:
\[\dfrac{b \leadsto b'}{\mathtt{if}\ b\ \mathtt{then}\ e_1\ \mathtt{else}\ e_2 \;\leadsto\; \mathtt{if}\ b'\ \mathtt{then}\ e_1\ \mathtt{else}\ e_2}\]
b to a valueb. The other branch is not evaluated.
ITT8060if 5 % 2 = 0 then 1 + 1 else 2 + 2
~> if 1 = 0 then 1 + 1 else 2 + 2
~> if false then 1 + 1 else 2 + 2
~> 2 + 2
~> 4
Interpreter gives the same result.
let ifExample =
if 5 % 2 = 0 then 1 + 1 else 2 + 2
// val ifExample: int = 4
ITT8060if\[\dfrac{b : \mathtt{bool} \qquad e_1 : t \qquad e_2 : t}{\mathtt{if}\ b\ \mathtt{then}\ e_1\ \mathtt{else}\ e_2 : t}\]
Both branches have the same type, and that is the type of the whole expression.
Type information is static. Before we execute the program, we do not
know whether b is going to be true or false.
Recall preservation: whichever evaluation rule applies, the type stays the same.
ITT8060
ITT8060Group together a fixed number of values of possibly different types.
Tuples are also called product types.
The Cartesian product of two sets \(A\) and \(B\), denoted \(A \times B\), is the set of all ordered pairs \((a, b)\) where \(a \in A\) and \(b \in B\).
\[ A \times B = \{ (a, b) \mid a \in A \land b \in B \} \]
Example: \(\{ A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K \} \times \{ ♡, ♢, ♧, ♤ \}\).
ITT8060Syntax: A \(k\)-tuple is constructed with \((e_1, ..., e_k)\).
Simplified evaluation rule:
\[\dfrac{e_i \leadsto e_i'}{(v_1, \ldots, v_{i-1}, e_i, \ldots, e_k) \; \leadsto \; (v_1, \ldots, v_{i-1}, e_i', \ldots, e_k)}\]
Values are tuples of the form \((v_1, ..., v_k)\) where each \(v_i\) is a value.
The rule fixes a left-to-right order: to evaluate \(e_i\), everything to its left must already be a value.
ITT8060(1 + 1, "a" + "b", true || false)
~> (2, "a" + "b", true || false)
~> (2, "ab", true || false)
~> (2, "ab", true)
Reduction steps agree with the interpreter.
let tupleExample =
(1 + 1, "a" + "b", true || false)
// val tupleExample: int * string * bool
// = (2, "ab", true)
ITT8060If \(t_1, \ldots, t_k\) are types, then so is \(t_1 * \cdots * t_k\).
\[\dfrac{e_1 : t_1 \qquad \cdots \qquad e_k : t_k}{(e_1, \ldots, e_k) : t_1 * \cdots * t_k}\]
t1 * (t2 * t3), t1 * t2 * t3 and (t1 * t2) * t3 are "almost" the
same, but not equal.
let flat : int * int * int = 1, 2, 3
let nested : int * (int * int) = 1, (2, 3)
let nested' : (int * int) * int = (1, 2), 3
ITT8060A 2-tuple is also called a pair.
let pair : string * int = "ab", 1
Two projections: fst : 'a * 'b -> 'a and snd : 'a * 'b -> 'b.
unit is the 0-tuple type. It has exactly one value, ().
let unitValue : unit = ()
ITT8060e; e'
ITT8060e; e'Read e; e' as e then e'. The ; is usually a newline.
\[v;\ e' \;\leadsto\; e' \quad (v \text{ a value}) \qquad \qquad \dfrac{e \leadsto e''}{e;\ e' \;\leadsto\; e'';\ e'}\]
e to a value v. Discard v.e' to a value v'. Final result: v'
Typing rule:
\[\dfrac{e : t \qquad e' : t'}{(e;\ e') : t'}\]
ITT80602 + 2; 1 + 1
~> 4; 1 + 1
~> 1 + 1
~> 2
> let seqExample = 2 + 2; 1 + 1;;
warning FS0020: The result of this
expression has type 'int' and is
implicitly ignored. ...
val seqExample: int = 2
The compiler accepts it and then warns us: computing 2 + 2 (a value
of type int) only to discard it is usually a mistake.
Why evaluate e and then discard the result? For its side effect.
Note that there is no warning if e : unit (i.e., we discard ()).
ITT8060fun x -> e
ITT8060We construct a function with argument x and body e as fun x -> e.
Example: fun r -> 3.14 * r * r.
fun x -> e introduces the variable x with scope e (similar to
let). Occurrences of x in e are no longer free.
(fun x -> x + y)[2/x] is unchanged.
(fun x -> x + y)[2/y] is (fun x -> x + 2).
No evaluation rules: fun x -> e is a value.
ITT8060\(s \rightarrow t\) is the type of functions from \(s\) to \(t\).
\[\dfrac{e : t \ (\text{ assuming } x : s)}{(\mathtt{fun}\ x \rightarrow e) : s \rightarrow t}\]
Example: assuming x : int, x % 2 = 0 : bool. Then fun x -> x % 2 = 0 is of type int -> bool.
Note that there are two different arrows here.
fun x -> e, the -> constructs a function value.s -> t, the -> constructs a function type.
ITT8060
ITT8060Function value applied to argument value: substitute.
\[(\mathtt{fun}\ x \rightarrow e)\ v \;\leadsto\; e[v/x] \qquad (v \text{ a value})\]
Function or argument not yet a value: they take a step, function first.
\[\dfrac{f \leadsto f'}{f\ a \;\leadsto\; f'\ a} \qquad \dfrac{a \leadsto a'}{(\mathtt{fun}\ x \rightarrow e)\ a \;\leadsto\; (\mathtt{fun}\ x \rightarrow e)\ a'}\]
f to fun x -> e.a to v.e[v/x].
ITT8060\[\dfrac{f : s \rightarrow t \qquad a : s}{f\ a : t}\]
If f : int -> bool and x : int, then f x : bool.
If f : int -> bool, then f "5" is a typing error. The function f
expects an argument of type int but the provided argument is a
string.
Typing errors are static.
ITT8060(fun r -> 3.14 * r * r) (1.0 + 1.0)
~> (fun r -> 3.14 * r * r) 2.0
~> (3.14 * r * r)[2.0/r]
~> 3.14 * 2.0 * 2.0
~> 12.56
let applyExample =
(fun r -> 3.14 * r * r) (1.0 + 1.0)
// val applyExample: float = 12.56
ITT8060let vs function applicationlet x = e in e'
~> let x = v in e' // e ~> v
~> e'[v/x]
(fun x -> e') e
~> (fun x -> e') v // e ~> v
~> e'[v/x]
Both evaluate e to a value and substitute it for x in e'.
ITT8060Functions are values: bind them with let.
let circleFun = fun r -> 3.14 * r * r
Shorthand: move the argument to the left of =, keep only the body on
the right.
let circle r = 3.14 * r * r
Annotations: on the argument, and after the argument list for the result.
let circleAnnotated (r : float) : float =
3.14 * r * r
ITT8060
ITT8060f 0 = 1
f (n + 1) = (n + 1) * f n
The function f is defined in terms of itself.
In F#, the function f needs to be in scope in its definition. This
requires the keyword rec.
let rec factIf n =
if n = 0
then 1
else n * factIf (n - 1)
Without rec, the factIf in the else branch is undefined (or
worse, refers to something else).
ITT8060> let bar n = -n;;
val bar: n: int -> int
> let bar n =
if n = 0 then 1 else n * bar (n - 1);;
val bar: n: int -> int
> bar 3;;
val it: int = -6
rec in the interpreterNo error, no warning.
Nothing wrong with the first bar.
The second bar refers to bar in its body (else branch). As the
definition is not let rec, the bar in the else branch is the bar
defined above.
bar 3 is 3 * (-2).
Duplicate definitions are not allowed at top level.
ITT8060fact 3fact 3
~> (fun n -> if n = 0 then 1 else n * fact (n - 1)) 3
~> (if n = 0 then 1 else n * fact (n - 1))[3/n]
~> if 3 = 0 then 1 else 3 * fact (3 - 1)
~> if false then 1 else 3 * fact (3 - 1)
~> 3 * fact (3 - 1)
~> 3 * fact 2
~> ...
~> 3 * (2 * (1 * fact 0))
~> 3 * (2 * (1 * (if n = 0 then 1 else n * fact (n - 1))[0/n]))
~> 3 * (2 * (1 * (if true then 1 else fact (0 - 1))))
~> 3 * (2 * (1 * 1))
~> 6
ITT8060let rec fact n = if n = 0 then 1 else n * fact (n - 1)
Base case: n = 0, no recursive call, result is 1.
Step case: n <> 0, recursive call fact (n - 1), result is n * fact (n - 1).
Typically, we want the step case to take as "closer" to the base case. When we reach the base case, the recursion terminates.
In this example, we only move closer to the base case when n > 0.
ITT8060let rec foo x = x * bar (x - 1)
let rec bar y =
if y <= 0 then 1 else y + foo (y - 1)
Separate let rec: bar is not in scope in the definition of foo.
let rec foo x = x * bar (x - 1)
and bar y =
if y <= 0 then 1 else y + foo (y - 1)
// foo 3 = 9
With and: we define both simultaneously; each may refer to the other.
ITT8060
ITT8060\(s\) and \(t\) in \(s \to t\) can themselves be function types.
\(\rightarrow\) is right-associative:
\[s \rightarrow t \rightarrow u \quad\text{stands for}\quad s \rightarrow (t \rightarrow u)\]
Application is left-associative:
\[f\ a\ b \quad\text{stands for}\quad (f\ a)\ b\]
Formally, every function takes one argument. The ability to return a function gives the impression of several arguments.
ITT8060(fun x -> (fun y -> x + y)) 2
~> (fun y -> x + y)[2/x]
~> fun y -> 2 + y : int -> int
(fun y -> 2 + y) 3
~> 2 + 3
~> 5 : int
let addTwo =
(fun x -> (fun y -> x + y)) 2
// val addTwo: (int -> int)
let curryExample = (fun x y -> x + y) 2 3
// val curryExample: int = 5
fun x y -> x + y is sugar for fun x -> (fun y -> x + y).
Let \(f : S \rightarrow T \rightarrow U\), \(s : S\) and \(t : T\). (Recall that \(S \to T \to U\) stands for \(S \to (T \to U)\).)
Then \(f\ s : T \rightarrow U\) and \(f\ s\ t : U\). (Recall that \(f\ s\ t\) stands for \((f\ s)\ t\).)
ITT8060match e with
ITT8060match expressionmatch e with
| p_1 [when c_1] -> e_1
...
| p_n [when c_n] -> e_n
Similar to if-then-else, but we can choose the patterns.
p_i patterns, c_i optional Boolean guards, e_i bodies.
At the moment, for us it is essentially a switch.
ITT8060\[\dfrac{v \text{ matches } p_i \text{ and } c_i \leadsto \mathtt{true}}{\mathtt{match}\ v\ \mathtt{with}\ \ldots \;\leadsto\; e_i} \qquad \dfrac{e \leadsto e'}{\mathtt{match}\ e\ \mathtt{with}\ \ldots \;\leadsto\; \mathtt{match}\ e'\ \mathtt{with}\ \ldots}\]
\[\dfrac{e : t \qquad p_i \text{ a pattern for } t \qquad c_i : \mathtt{bool} \qquad e_i : u}{(\mathtt{match}\ e\ \mathtt{with}\ \ldots) : u}\]
Patterns \(p_i\) may introduce bindings that are in scope for \(c_i\) and \(e_i\).
\(\mathtt{match}\ v\ \mathtt{with}\ \ldots \;\leadsto\; e_i[v_1/x_1]\ldots[v_k/x_k]\) when
where \(x_i \mapsto v_i\) are the bindings introduced by \(p_i\).
ITT8060let classify p =
match p with
| (0, 0) -> "Both zero"
| (x, y) when x % 2 = 0 -> "x even, y is: " + string y
| w -> "Whatever: " + string p
(0, 0) — matches when both components zero, introduces no bindings.
(x, y) when … — matches any pair; binds x and y to first and second component. What is their scope?
w — any pair; binds w to the whole pair.
ITT8060classify (1 + 1, 2 + 2)classify (1 + 1, 2 + 2)
~> classify (2, 4)
~> match (2, 4) with
| (0, 0) -> "Both zero"
| (x, y) when x % 2 = 0 -> "x even, y is: " + string y
| w -> "Whatever: " + string (2, 4)
(0, 0) fails. (x, y) matches with x = 2, y = 4; the guard (x % 2 = 0)[2/x][4/y] ~> 2 % 2 = 0 ~> true.
~> ("x even, y is: " + string y)[2/x][4/y]
~> "x even, y is: " + string 4
~> "x even, y is: 4"
ITT8060let s = 100
let isEqualTo100 x =
match x with
| s -> "Yes!"
| _ -> "No."
// isEqualTo100 1 = "Yes!"
warning FS0026:
This rule will never be matched
val isEqualTo100: x: 'a -> string
An identifier in a pattern introduces a binding — this s is a
new variable, the pattern matches everything, and shadows the outside
s.
The compiler notices the second case/pattern is unreachable, and
infers 'a -> string as the type. Why?
The pattern _ matches everything but does not introduce any bindings.
ITT8060let rec fact n =
match n with
| 0 -> 1
| n' -> n' * fact (n' - 1)
let (x, y) = (1, 3)
let fst (x, y) = x
let snd (_, y) = y
Factorial with match: the base case is the literal pattern 0.
Patterns also work in let bindings and in function parameters.
fact 3
~> match 3 with ...
~> (n' * fact (n' - 1))[3/n']
~> 3 * fact (3 - 1)
~> 3 * fact 2
~> ...
ITT8060
ITT8060abs : int -> int
(+) : int -> int -> int
not : bool -> bool
(&&) : bool -> bool -> bool
(||) : bool -> bool -> bool
float : int -> float
A binary operator in parentheses is a prefix function: (+) 1 2 evaluates to 1 + 2.
\[\mathtt{false}\ \mathtt{\&\&}\ b_2 \leadsto \mathtt{false}\]
\[\mathtt{true}\ \mathtt{\&\&}\ b_2 \leadsto b_2\]
\[\dfrac{b_1 \leadsto b_1'}{b_1\ \mathtt{\&\&}\ b_2 \leadsto b_1'\ \mathtt{\&\&}\ b_2}\]
ITT8060&& versus a functionlet shortCircuit =
false && (failwith "evaluated!")
// val shortCircuit: bool = false
> (fun x y -> x && y)
false (failwith "evaluated!");;
System.Exception: evaluated!
Stopped due to error
Isn't b1 && b2 the same as (fun x y -> x && y) b1 b2?
No. The rule for false && b2 discards b2 without evaluating
it. Function application evaluates both arguments to values and
then substitutes into the body.
ITT8060
ITT8060type Position = int * int
let origin : Position = (0, 0)
let names values, type names types
Position is an alias for int * int.
e : Position then also e : int * int but the former is often more readable.
ITT8060let mystery x =
let (i, b) = x in
not b, i + 1
Task: find U such that mystery : U.
mystery is a function. U = S -> T, x : S, and not b, i + 1 : T.
x matched against a pair pattern. S = S1 * S2, i : S1, b : S2.
Result is a pair: T = T1 * T2, not b : T1, i + 1 : T2.
not : bool -> bool, so T1 = bool and S2 = bool.
(+) : int -> int -> int, so T2 = int and S1 = int.
ITT8060U = S -> T
S = S1 * S2
T = T1 * T2
S2 = bool
T1 = bool
S1 = int
T2 = int
U = S1 * S2 -> T1 * T2
= int * bool -> bool * int
mystery : int * bool -> bool * int
> let mystery x =
let (i, b) = x in
not b, i + 1;;
val mystery: int * bool -> bool * int
ITT8060Reading: Hansen & Rischel, chapters 1 and 2.
Coursework 1 is out this week.
Full prose version with runnable examples: notes.html. The script is 02-types-expressions-functions.fsx in course-materials.
ITT8060