OCaml

A Functional Programming Crash Course

Type Inference Pattern Matching Algebraic Types Higher-Order Functions
let rec factorial n =
if n = 0 then 1
else n * factorial (n - 1)

Use Arrow Keys to navigate • Interactive playgrounds throughout

Why OCaml for Theory?

Functional programming mirrors mathematical reasoning

Perfect match for CS theory

  • Immutability — like math, values don't change
  • Pattern matching — natural for defining automata transitions
  • Algebraic types — model grammars, trees, and ASTs directly
  • Recursion — the primary looping mechanism, just like inductive proofs

Think of it this way

In Java/Python, you write instructions. In OCaml, you write definitions — just like math. let f x = x + 1 reads like "f(x) = x + 1".

Getting Started: The REPL

OCaml has an interactive toplevel — type expressions, see results instantly

# Welcome to the OCaml REPL simulator!
Type an expression and press Enter.

The ;; matters!

In the REPL, ;; tells OCaml "evaluate this now." In files, it's optional (and usually omitted).

No implicit conversions

OCaml won't convert int to float automatically. Use float_of_int, int_of_float, etc.

Even arithmetic differs: + for ints, +. for floats!

Try these expressions:

  • 3 + 4;; → int arithmetic
  • "hello" ^ " world";; → string concat
  • true && false;; → boolean logic
  • let x = 42;; → binding

Basic Types and Values

OCaml is statically typed — every expression has a type known at compile time

Core Types

TypeExampleOperations
int42, -7, 0+, -, *, /, mod
float3.14, 2.0+., -., *., /.
booltrue, false&&, ||, not
char'a', 'Z'Char.code
string"hello"^, String.length
unit()(side effects)

int vs float — separate operators!

3 + 4 is int addition. 3.0 +. 4.0 is float addition. Mixing them is a type error.

Let Bindings

Binding names to values — NOT variable assignment!

let x = 10
let y = x + 5 (* y = 15 *)
let x = 20 (* new binding, NOT mutation! *)
let z = x + y (* z = 35 *)
Environment: { }
(* let ... in for local scope *)
let result =
let a = 5 in
let b = a * 2 in
a + b (* = 15 *)

Bindings, not variables

let x = 10 means "x is 10" — not "store 10 in a box called x." You're creating a new name in the environment.

Shadowing ≠ Mutation

When you write let x = 20 after let x = 10, the old x still exists — it's just hidden ("shadowed"). Any code that captured the old x still sees 10.

let ... in = local scope

let a = 5 in a + 1 means "in the expression a+1, let a be 5." The binding only lives inside the in expression.

Functions

Functions are values — define them, pass them, return them

(* Named function *)
let double x = x * 2
(* Anonymous function (lambda) *)
let double = fun x -> x * 2
(* Multi-argument = curried *)
let add x y = x + y
(* Same as: let add = fun x -> fun y -> x + y *)
(* Partial application *)
let add5 = add 5
(* add5 10 = 15 *)

All functions take exactly ONE argument

let add x y = x + y is actually syntactic sugar for a function that takes x and returns another function that takes y.

Type: int -> int -> int = int -> (int -> int)

Currying

Named after Haskell Curry. A 2-argument function is really a 1-argument function that returns a 1-argument function. This enables partial application.

No parentheses needed!

f x y not f(x, y). Parentheses are only for grouping: f (g x) means "apply g to x, then apply f to the result."

Type Inference

OCaml figures out the types — you rarely write them

Hindley-Milner Algorithm

OCaml uses constraint-based inference. It looks at how values are used and deduces their types. No annotations needed!

(* OCaml infers all of these *)
let x = 42 (* int *)
let f x = x + 1 (* int -> int *)
let g x y = x ^ y (* string -> string -> string *)
let id x = x (* 'a -> 'a (polymorphic!) *)

'a = type variable (polymorphic)

'a -> 'a means "takes any type, returns the same type." Like Java generics but inferred automatically.

If-Then-Else as Expressions

In OCaml, if-then-else RETURNS a value — it's an expression, not a statement

(* This RETURNS a value *)
let abs_val x =
if x >= 0 then x
else -x
(* Can be used anywhere an expression goes *)
let msg = "value is " ^ (if x > 0 then "positive" else "non-positive")
(* Both branches MUST have the same type *)
let bad x =
if x > 0 then "yes"
else 42 (* TYPE ERROR! *)

Everything is an expression

Unlike Java/Python where if is a statement, in OCaml it's an expression that produces a value. Like the ternary ? : in Java, but this is the ONLY if.

Both branches must match

If the then branch is string, the else branch must also be string. OCaml enforces this at compile time.

Compare to Java

// Java: statement
if (x > 0) result = "yes";
else result = "no";
(* OCaml: expression *)
let result = if x > 0 then "yes" else "no"

Recursion — Loops Don't Exist

In functional programming, recursion replaces iteration entirely

Factorial Step-Through

Click Step to trace the recursion...
Key Pattern: Every recursive function needs a base case and a recursive case that moves toward it.

Tuples & Records

Grouping related values together

Tuples — Fixed-size, Heterogeneous

(* Tuples: use * in types *)
let pair = (3, "hello") (* int * string *)
let triple = (1, 2.0, true) (* int * float * bool *)
(* Destructure with pattern matching *)
let (x, y) = pair (* x=3, y="hello" *)
let (a, _, c) = triple (* a=1, c=true, ignore 2nd *)
(* Function returning a tuple *)
let min_max lst =
(List.fold_left min max_int lst,
List.fold_left max min_int lst)
Analogy: Tuples are like coordinates — (3, 7) is a point. The position matters, the name doesn't.

Records — Named Fields

(* Define a record type *)
type student = {
name : string;
gpa : float;
year : int
}
(* Create a record *)
let s = { name = "Alice"; gpa = 3.8; year = 3 }
(* Access fields with dot notation *)
let n = s.name (* "Alice" *)
(* Functional update (copy with changes) *)
let s2 = { s with year = 4 } (* new record! *)
Immutable by default: { s with year = 4 } creates a new record — s is unchanged. This is how functional programming avoids side effects.

Lists — The Core Data Structure

Singly-linked, immutable, homogeneous

(* Three ways to write the same list *)
let a = [1; 2; 3]
let b = 1 :: 2 :: 3 :: []
let c = 1 :: (2 :: (3 :: []))
(* :: is "cons" — prepend O(1) *)
let d = 0 :: a (* [0;1;2;3] *)
(* Pattern matching on lists *)
let head_or_zero lst =
match lst with
| [] -> 0
| h :: _ -> h
Click a button to see the visualization
No random access! Lists are linked — accessing the nth element is O(n). Use arrays if you need index-based access.

Challenge A — Predict the Output

Test your understanding of recursion and lists

What does this evaluate to?

let rec mystery lst =
match lst with
| [] -> []
| h :: t -> mystery t @ [h]
let result = mystery [1; 2; 3; 4]

Trace

Click "Step Through" to see the trace...
Hint: The @ operator appends two lists. What does placing h at the end of the recursive result do?

Pattern Matching — OCaml's Superpower

Destructure data and branch in one elegant construct

Interactive Pattern Matcher

Exhaustiveness: OCaml checks that your patterns cover ALL cases at compile time. Missing a case? You get a warning — not a runtime crash.

Algebraic Data Types (Variants)

Define your own types with multiple cases — OCaml's killer feature

(* Enumeration variant *)
type color = Red | Green | Blue
(* Variant with data *)
type shape =
| Circle of float
| Rect of float * float
| Triangle of float * float * float
(* Pattern match to handle each case *)
let area s =
match s with
| Circle r -> Float.pi *. r *. r
| Rect (w,h) -> w *. h
| Triangle (a,b,c) ->
let s = (a+.b+.c)/.2. in
sqrt(s*.(s-.a)*.(s-.b)*.(s-.c))
Analogy: Variants are tagged unions — like a package that says "I'm a Circle with radius 5" or "I'm a Rect with width 3, height 4". The tag tells you which case it is, and the data rides along.
Click a shape to see the area calculation
Why this matters: If you add a new variant case (e.g., Pentagon), the compiler will warn you about every match that doesn't handle it. No forgotten cases, no null pointer exceptions.

Challenge B — Fix the Bug

This function has a subtle pattern matching error

Buggy Code

(* Should return the second element, or 0 *)
let second lst =
match lst with
| [] -> 0
| h :: t -> t

The Fix

Select the bug and click Check to see the corrected code...
Common trap: In h :: t, h is the first element (a value) but t is the rest of the list (still a list!). They have different types.

The Option Type — No More Null

OCaml's elegant solution to the "billion-dollar mistake"

(* Built-in variant type *)
type 'a option = None | Some of 'a
(* Safe division — no exceptions! *)
let safe_div a b =
if b = 0 then None
else Some (a / b)
(* Must handle both cases *)
let result =
match safe_div 10 3 with
| None -> "undefined"
| Some v -> string_of_int v
(* Find in a list *)
let rec find_first pred lst =
match lst with
| [] -> None
| h :: t ->
if pred h then Some h
else find_first pred t
Why Option beats null: You cannot forget to check. The compiler forces you to handle None. In Java, you can call .toString() on null and get a runtime crash. In OCaml, that's a compile error.
Rule: Never use exceptions for expected failures (missing keys, division by zero). Use option instead — it makes failure explicit in the type.

Higher-Order Functions

Functions that take functions as arguments or return functions

(* A function that takes a function *)
let apply_twice f x = f (f x)
(* apply_twice : ('a -> 'a) -> 'a -> 'a *)
let add1 x = x + 1
let double x = x * 2
apply_twice add1 5 (* = 7 *)
apply_twice double 3 (* = 12 *)
(* Anonymous functions with fun *)
apply_twice (fun x -> x * x) 2 (* = 16 *)
Analogy: A higher-order function is like a recipe that says "do something to the chicken" — the "something" (grill, fry, bake) is a parameter you fill in.

Step Through: apply_twice

Functions are values: In OCaml, add1 is a value just like 42. You can pass it around, store it, return it.

Map, Filter, Fold — The Big Three

Three higher-order functions that replace most loops

The power of composition: These three functions + the pipe operator |> let you express complex data transformations as clear, readable pipelines — no mutable variables, no loop counters.

Challenge C — Pick the Right HOF

Which higher-order function should you use?

1. Double every element

"Given [1;2;3], produce [2;4;6]"

2. Sum of all elements

"Given [1;2;3;4], produce 10"

3. Keep only positives

"Given [-1;2;-3;4], produce [2;4]"

Decision Rule: Map = same length, different values. Filter = fewer elements, same values. Fold = reduce to one value.

Recursive Types — Trees

Types that reference themselves — the foundation of tree structures

(* A binary tree type *)
type 'a tree =
| Leaf
| Node of 'a * 'a tree * 'a tree
(* Example tree *)
let t = Node(1,
Node(2, Leaf, Leaf),
Node(3,
Node(4, Leaf, Leaf),
Leaf))
(* Recursive functions on trees *)
let rec size = function
| Leaf -> 0
| Node(_, l, r) -> 1 + size l + size r
Pattern: Recursive type → recursive function. Every tree function matches Leaf (base) and Node (recurse on left/right).

Connections & Summary

How OCaml connects to CS305 and the bigger picture

OCaml ↔ Theory of Computation

Why learn OCaml in CS336? Pattern matching mirrors formal proofs by cases. Algebraic types model language grammars. Recursion is the natural way to process recursive structures like parse trees and automata.

OCaml Cheat Sheet

let x = 5
binding
let f x = x+1
function
fun x -> x+1
anonymous fn
let rec f n = ...
recursion
match x with
pattern match
type t = A | B
variant
h :: t
cons/destructure
x |> f |> g
pipe
List.map f lst
map
List.fold_left
fold
None | Some v
option type
(a, b, c)
tuple
Core principles: Everything is an expression. Types are inferred. Data is immutable. Functions are values.

Quiz — Multiple Choice

Q1: Type of fun x -> x

Q2: What is List.map?

Q3: let (a,_,c) = (1,2,3)

What are a and c?

Quiz — Trace the Recursion

What does this function return?

let rec count f lst =
match lst with
| [] -> 0
| h :: t ->
(if f h then 1 else 0) + count f t
let result =
count (fun x -> x > 3) [1; 5; 2; 7; 3; 8]

Step-by-Step Trace

Enter your answer, then click Check to see the trace.

Quiz — Predict the Pipeline

What does this pipeline produce?

type shape = Circle of float | Rect of float * float
let area = function
| Circle r -> Float.pi *. r *. r
| Rect (w,h) -> w *. h
let shapes = [Circle 1.0; Rect(3.0,4.0);
Circle 2.0; Rect(1.0,1.0)]
let result =
shapes
|> List.map area
|> List.filter (fun a -> a > 5.0)
|> List.length

Pipeline Breakdown

Enter your answer, then click Check to see the pipeline stages.