Discriminated unions, lists and higher-order functions

ITT8060 Advanced Programming · Autumn 2026

Tallinn University of Technology

Full prose version with runnable examples: notes.html

ITT8060

Overview

  • Part I: Discriminated unions and records
  • Part II: Lists
  • Part III: Higher-order list functions
ITT8060

Recall the match expression

match e with
| p_1 [ when c_1 ] -> e_1
| ...
| p_n [ when c_n ] -> e_n

Match e against the patterns from top to bottom. If p_i matches, evaluate the guard c_i (missing guard = when true).

If c_i is true, then e_i is the result of the whole match. Otherwise continue with the next pattern.

At most one e_i gets evaluated. The type of the match is the type of the e_i.

ITT8060

An example with "holes"

let rec foo (xs: int list) (n: int) : int =
  let y = ?1
  match xs with
  | []               -> ?2
  | x :: xs' when ?3 -> ?4
  | _                -> ?5

What are the "holes", and what can we fill them with? Two questions: the type the "hole" must have, and the names in scope there.

Hole  Type      In scope
?1    any type  xs, n, foo, ...
?2    int       xs, n, foo, y, ...
?3    bool      xs, n, foo, y, x, xs', ...
?4    int       xs, n, foo, y, x, xs', ...
?5    int       xs, n, foo, y, ...
ITT8060

Part I

Discriminated unions

ITT8060

Motivation

We want a list of all addresses of a contact. An address can be:

type PhysAddr = string * string * string
type VirtAddr = string * string

let a1 : PhysAddr = "Ehitajate tee", "5", "Tallinn"
let a2 : VirtAddr = "juulius", "ttu.ee"

But a1 :: a2 :: [] does not type-check. (Why?)

Goal: a type in which we can combine the two address types. Informally:

type Address = PhysAddr + VirtAddr

The + is not valid F#. This is informal notation for the disjoint union of the values of the two types: a value of type Address is either a physical or a virtual address, not both.

ITT8060

Standard lists

We draw the values

ICons (1, ICons (2, ICons (3, INil)))

and

1 :: 2 :: 3 :: []

as trees (of constructor applications).

They represent the same thing.

The built-in list type is a recursive discriminated union with constructors [] and ::.

ITT8060

The option type

Intuitively, take an existing type and add an additional value to it, None. Used extensively in the standard library to represent the lack of a value of type 'a.

// in the standard library:
// type 'a option = None | Some of 'a

let noInt: int option = None
let anInt: int option = Some 11

In Java, the class Integer has as values all int values plus null. Every reference type has null as a value. With option we can be more precise.

Different point of view: 'a option is the type of lists of length ≤ 1. (How?)

ITT8060
let rec indexOf (el : 'a)
                (xs : 'a list)
                : int option =
  match xs with
  | []       -> None
  | x :: xs' ->
    if x = el then
      Some 0
    else
      match indexOf el xs' with
      | None   -> None
      | Some i -> Some (i + 1)

indexOf 3 [5; 3; 8]   // Some 1
indexOf 7 [5; 3; 8]   // None

Example: indexOf

Find the index of the first occurrence of el in xs.

Why return int option? What should the index of the first occurrence be when el is not in xs?

When xs is empty, then we know the result. Otherwise, el is the head, or, it is somewhere in the tail and we match on the result of the recursive call.

Why return Some (i + 1)?

ITT8060

A first look at records

the rest next week

ITT8060
let describe (p: Person) : string =
  match p with
  | { name = ""; age = _ } -> "empty string"
  | { name = n; age = a } ->
    $"Name is {n} and age is {a}"

type 'a NotList = { head: 'a; tail: 'a NotList }

let rec x = { head = 1
              tail = { head = 2; tail = x } }
x.head             // 1
x.tail.head        // 2
x.tail.tail.head   // 1
> let rec xs = 1 :: 2 :: xs;;
error FS0260: Recursive values cannot appear
directly as a construction of the type
'List`1' within a recursive binding. This
feature has been removed from the F# language.

More records

Record literals can be used as patterns.

Records can be recursive and parameterised.

Every NotList has a tail that is again a NotList (which has a head element).

Not allowed for lists (but allowed with our PList).

ITT8060

Part II

Lists

ITT8060

Part II: Lists

  • Generating lists using range expressions
  • Example functions on lists: append, reverse, isMember
  • Typical recursion patterns on lists
ITT8060

Range expressions

A simple range expression \([b\ \mathtt{..}\ e]\), where \(e \geq b\), generates the list

\([b;\ b+1;\ \ldots;\ b+n]\)

where \(b + n \leq e < b + n + 1\). Empty when \(e < b\).

[ -3 .. 5 ]   // [-3; -2; -1; 0; 1; 2; 3; 4; 5]
[ 7 .. 4 ]    // []

With specified step:

\([b\ \mathtt{..}\ s\ \mathtt{..}\ e] = [b;\ b+s;\ \ldots;\ b+ns]\)

The list is ascending if \(s > 0\) and descending if \(s < 0\).

[ 6 .. -1 .. 2 ] // [6; 5; 4; 3; 2]
[ 2 ..  2 .. 6 ] // [2; 4; 6]
ITT8060

Range expressions with floats

[ 0.0 .. System.Math.PI / 2.0 .. 2.0 * System.Math.PI ]
// [0.0; 1.570796327; 3.141592654; 4.71238898; 6.283185307]
[ 2.4 .. 3.0 ** 1.7 ] // [2.4; 3.4; 4.4; 5.4; 6.4]
3.0 ** 1.7            // 6.47300784

Beware of precision loss when summing floats.

ITT8060

Simple recursion on lists

Three simple functions — append, reverse, isMember — whose declarations follow the structure of the list argument:

let rec f ... xs ... =
  match xs with
  | []       -> v
  | x :: xs' -> ... f xs' ...

What is the structure of 1 :: 2 :: 3 :: 4 :: 5 :: []?

let xs = 1 :: (2 :: (3 :: (4 :: (5 :: []))))

List.head xs = 1
List.tail xs = 2 :: (3 :: (4 :: (5 :: [])))

The tail of a nonempty list is a recursive substructure.

ITT8060

Append

The infix operator @ joins two lists:

\([x_1;\ldots;x_m]\ \mathtt{@}\ [y_1;\ldots;y_n] = [x_1;\ldots;x_m;\ y_1;\ldots;y_n]\)

Properties:

  • \([\,]\ \mathtt{@}\ ys = ys\)
  • \([x_1;\ \ldots;\ x_m]\ \mathtt{@}\ ys = x_1\ \mathtt{::}\ ([x_2;\ \ldots;\ x_m]\ \mathtt{@}\ ys)\)
let rec append xs ys =
  match xs with
  | [] -> ys
  | x :: xs' -> x :: append xs' ys

append [1; 2] [3; 4]   // [1; 2; 3; 4]

(@) : 'a list -> 'a list -> 'a list is available in the standard library.

ITT8060
append [1; 2] [3; 4]
~> 1 :: (append [2] [3; 4])
~> 1 :: (2 :: (append [] [3; 4]))
~> 1 :: (2 :: [3; 4])
~> 1 :: [2; 3; 4]
~> [1; 2; 3; 4]

Append: evaluation

ys is never taken apart.

Execution time is linear in the size of the first list.

Be careful with usage like: xs @ [x].

ITT8060

Reverse

let rec naiveRev lst =
  match lst with
  | [] -> []
  | x :: xs -> naiveRev xs @ [x]

// val naiveRev: 'a list -> 'a list

naiveRev [1; 2; 3]   // [3; 2; 1]
naiveRev [1; 2; 3]
~> naiveRev [2; 3] @ [1]
~> (naiveRev [3] @ [2]) @ [1]
~> ((naiveRev [] @ [3]) @ [2]) @ [1]
~> (([] @ [3]) @ [2]) @ [1]
~> ([3] @ [2]) @ [1]
~> ...
~> [3; 2; 1]

\(\mathtt{naiveRev}\ [x_1;\ x_2;\ \ldots;\ x_n] = [x_n;\ \ldots;\ x_2;\ x_1]\).

naiveRev performs an @ for every element in the list. Recall that @ is linear in the first argument.

Therefore, naiveRev is \(O(n^2)\) time.

Built-in List.rev is \(O(n)\). See fold in Part III for another linear implementation.

ITT8060

Match on results of the recursive call

let rec f ... xs ... =
  ...
  let pat(y) = f xs
  e(y)

Recall unzip from last week (lecture 3 notes): let (xs, ys) = unzip rest.

Two more examples: sumProd and split.

ITT8060

Example: sumProd

\(\mathtt{sumProd}\ [x_0;\ \ldots;\ x_{n-1}] = (x_0 + \cdots + x_{n-1},\ x_0 \cdots x_{n-1})\)

Recursion formula: \(\mathtt{sumProd}\ [x_0;\ \ldots;\ x_{n-1}] = (x_0 + \mathit{rSum},\ x_0 \cdot \mathit{rProd})\) where \((\mathit{rSum}, \mathit{rProd}) = \mathtt{sumProd}\ [x_1;\ \ldots;\ x_{n-1}]\).

Base case \((0, 1)\): the neutral elements.

let rec sumProd lst =
  match lst with
  | [] -> (0, 1)
  | x :: rest -> let (rSum, rProd) = sumProd rest
                 (x + rSum, x * rProd)
// val sumProd: int list -> int * int

sumProd [2; 5]   // (7, 10)
ITT8060

Example: split

\(\mathtt{split}\ [x_0;\ x_1;\ x_2;\ x_3;\ \ldots] = ([x_0;\ x_2;\ \ldots],\ [x_1;\ x_3;\ \ldots])\)

let rec split lst =
  match lst with
  | [] -> ([], [])
  | [x] -> ([ x ], [])
  | x :: y :: xs -> let (xs1, xs2) = split xs
                    (x :: xs1, y :: xs2)

split [1 .. 7]   // ([1; 3; 5; 7], [2; 4; 6])

Notice a convenient division into three cases, and the recursion formula \(\mathtt{split}\ [x_0;\ x_1;\ x_2;\ \ldots] = (x_0\ \mathtt{::}\ \mathit{xs1},\ x_1\ \mathtt{::}\ \mathit{xs2})\)

where \((\mathit{xs1}, \mathit{xs2}) = \mathtt{split}\ [x_2;\ \ldots]\).

ITT8060

Part III

Higher-order list functions

ITT8060

Higher-order functions

Functions that accept a function as an argument or return a function as a result.

They are everywhere: \(\sum_{i=a}^{b} f(i),\quad \frac{df}{dx},\quad \{x \in A \mid P(x)\},\ \ldots\)

They are expressive and useful.

ITT8060

Now down to earth

Many recursive declarations follow the same schema:

let rec f xs =
  match xs with
  | []       -> ...
  | x :: xs' -> ... (f xs') ...
  • Goal: avoid repeating (almost) identical code fragments
  • Solution: capture frequently occurring patterns as higher-order functions
  • Result: more succinct and reusable declarations

Today: map, filter, fold, foldBack, exists, forall, tryFind.

All of these are available in the List module; we try to define each one of them.

ITT8060

Two simple list functions

let rec add n xs =
  match xs with
  | []       -> []
  | x :: xs' -> n + x :: add n xs'

// add: int -> int list -> int list

add 5 [-2; -1; 0; 1; 2]
//    [ 3;  4; 5; 6; 7]
let rec flip xs =
  match xs with
  | []       -> []
  | x :: xs' -> -1 * x :: flip xs'

// flip: int list -> int list

flip [-2; -1; 0;  1;  2]
//   [ 2;  1; 0; -1; -2]

The only difference: what happens to the elements (n + x vs -1 * x). The lists are processed in exactly the same way.

How to represent what happens to an element in the list?

With a function of type int -> int: fun x -> n + x vs fun x -> -1 * x.

ITT8060

Exercise

Declare a function \(\mathtt{g}\ [x_1;\ \ldots;\ x_n] = [x_1^2 + 1;\ \ldots;\ x_n^2 + 1]\)

Remember \(\mathtt{map}\ f\ [v_1;\ \ldots;\ v_n] = [f\ v_1;\ \ldots;\ f\ v_n]\).

There is also a map for option types. What does it do?

Option.map : ('a -> 'b) -> 'a option -> 'b option
ITT8060

Something similar to map

List.map : ('a -> 'b) -> ('a list -> 'b list)

map constructs a function 'a list -> 'b list from 'a -> 'b.

List.collect : ('a -> 'b list) -> ('a list -> 'b list)

List.collect constructs a function of the same type from a function that transforms an element into a list. What does it do?

List.collect (fun x -> [0; x]) [1 .. 5]  // [0; 1; 0; 2; 0; 3; 0; 4; 0; 5]
ITT8060

Filtering

Set comprehension: \(\{x \in xs : p(x)\}\)

filter p xs is the sublist of those elements x in xs for which p x = true. Library function: List.filter.

let rec filter p xs =
  match xs with
  | [] -> []
  | x :: xs' -> if p x
                then x :: filter p xs'
                else filter p xs'

// val filter: ('a -> bool) -> 'a list -> 'a list

filter System.Char.IsLetter [ '2'; 'p'; 'F'; '-' ]  // ['p'; 'F']

System.Char.IsLetter c is true iff c is a letter.

ITT8060

Combining a list into one value

let rec sum (xs: int list) : int =
  match xs with
  | []       -> 0
  | x :: xs' -> x + sum xs'

sum [1; 2; 3]   // 6
let rec prodabs (xs: int list) : int =
  match xs with
  | []       -> 1
  | x :: xs' -> abs x * prodabs xs'

prodabs [-2; 3; -4]   // 24

The two functions are very similar.

Differences: base value (0 vs 1), and how to combine the element x with the recursive result (let's call it r).

What do the following functions represent?

  • fun x r -> x + r
  • fun x r -> abs x * r
ITT8060

A map via foldBack, evaluated

let fn = fun x r -> abs x :: r
mapB abs [-1; 2; -3]
=  foldBack fn [-1; 2; -3] []
~> abs -1 :: foldBack fn [2; -3] []
~> abs -1 :: abs 2 :: foldBack fn [-3] []
~> abs -1 :: abs 2 :: abs -3 :: foldBack fn [] []
~> abs -1 :: abs 2 :: abs -3 :: []
~> 1      :: 2     :: 3      :: []

Each element is combined with the recursive result (folded tail) using the function fn. As fn applies the function abs to the element and cons-es it in front of the recursive result, we have map.

ITT8060

foldBack versus fold

\(\mathtt{foldBack}\ (\otimes)\ [a_0;\ a_1;\ a_2]\ e\)

Note how the tree leans to the right. Why? What does this mean?

\(\mathtt{fold}\ (\oplus)\ e\ [a_0;\ a_1;\ a_2]\)

Note how the tree leans to the left. Why?

ITT8060

Exercise

Define the function isMember (that decides list membership) in terms of exists.

let isMember x ys = exists ??? ???
val isMember: 'a -> 'a list -> bool when 'a: equality

Remember: exists p xs = true precisely when there is some x in xs such that p x = true.

ITT8060

Exercises

Define the function disjoint : 'a list -> 'a list -> bool so that disjoint xs ys is true when there are no common elements in xs and ys, and false otherwise.

Define the function subset : 'a list -> 'a list -> bool so that subset xs ys is true when every element of xs is also an element of ys, and false otherwise.

Define the function inter : 'a list -> 'a list -> 'a list so that x is an element of inter xs ys precisely when x is an element of both xs and ys.

Note that inter produces a list. How should the elements be ordered? Are duplicates allowed?

When appropriate, try to use the functions defined earlier.

ITT8060

tryFind

Given a predicate p, find an element x from a list xs such that p x = true.

let rec tryFind p xs =
  match xs with
  | []                -> None
  | x :: xs' when p x -> Some x
  | _ :: xs'          -> tryFind p xs'
// val tryFind: ('a -> bool) -> 'a list -> 'a option

tryFind (fun x -> x > 3) [ 1; 5; -2; 8 ]   // Some 5

The result type is 'a option. Why?

When is the result None and when is it Some x?

ITT8060

Exercise: union of sets

We represent a set as a list with unique elements (without duplicates).

Uniqueness is an invariant that the operations must preserve: if xs and ys are sets (lists wihtout duplicates) and f is a set-operation, then f xs ys must also be a set (list without duplicates).

Define the set-operation insert : 'a -> 'a list -> 'a list that corresponds to the following operation on sets: \(x \cup X\).

In other words, insert x xs should represent the set \(x \cup X\) where \(X\) is represented by the list xs.

What should be the result when x is already in xs? Does the order of elements matter?

Define the set-operation union : 'a list -> 'a list -> 'a list.

ITT8060

Question

We know map : ('a -> 'b) -> 'a list -> 'b list.

With 'a = int and 'b = int its type is (int -> int) -> int list -> int list.

Not every function of type (int -> int) -> int list -> int list can be defined as map f xs where f : int -> int and xs : int list.

Is this good or bad?

ITT8060

Next

Reading (mandatory): Hansen & Rischel, chapter 3 (tagged values and records), chapter 4, and the beginning of chapter 5 (lists).

Homework 3 is issued this week.

ITT8060

Appendix I

Some algebraic laws for higher-order functions

Where we assume that functions are pure.

ITT8060

Appendix II

shapes as a disjoint union

ITT8060
type Shape =
  | Circle of float
  | Square of float
  | Triangle of float * float * float

Circle 2.0                     // Shape
Triangle (1.0, 2.0, 3.0)       // Shape
Square 4.0                     // Shape

Disjoint sets — an example

A shape is either a circle, a square or a triangle — the union of three disjoint sets.

The tags Circle, Square and Triangle are constructors.

ITT8060
let area s =
  match s with
  | Circle r -> System.Math.PI * r * r
  | Square a -> a * a
  | Triangle (a, b, c) ->
    let s = (a + b + c) / 2.0
    sqrt (s * (s - a) * (s - b) * (s - c))

// val area: s: Shape -> float

area (Circle 1.2)              // 4.523893421

Constructors in patterns

A shape-area function, declared following the structure of shapes.

A constructor only matches itself; matching binds the carried values:

area (Circle 1.2)
~> System.Math.PI * r * r   // r ↦ 1.2
~> ...
ITT8060