OCaml

A Functional Programming Crash Course for CS Theory

___ ____ _ / _ \ / ___|__ _ _ __ ___ | | | | | | | / _` | '_ ` _ \| | | |_| | |__| (_| | | | | | | | \___/ \____\__,_|_| |_| |_|_| CS305 / CS336 - Formal Languages & Theory of Computation

Use arrow keys or buttons to navigate. Press 'S' to reveal steps.

1 / 22

Why OCaml for Theory?

Functional programming and formal languages are deeply connected

The Connections

  • Pattern matching = state transitions in automata
  • Recursive types = grammar productions in CFGs
  • Immutability = mathematical functions (same input, same output)
  • Recursive functions = recursive descent parsing
  • Algebraic data types = abstract syntax trees
Theory Concept OCaml Feature ─────────────── ───────────── DFA transitions --> match state, symbol with CFG productions --> type expr = Add of expr * expr Parse trees --> recursive data types Language membership -> recursive functions Math functions --> pure functions (no side effects)

Key Idea

OCaml lets you write code that looks like the math you see in theory class. A DFA simulator in OCaml reads almost identically to the formal definition (Q, Σ, δ, q0, F).

Analogy: Why Not Java/Python?

Writing a DFA simulator in Java is like translating Shakespeare into emojis -- it works, but the beauty of the original structure is lost. OCaml preserves the mathematical elegance.

2 / 22

Getting Started

Installing OCaml and your first interaction

Installation

# Install opam (OCaml package manager) $ brew install opam # macOS $ sudo apt install opam # Ubuntu/Debian # Initialize opam $ opam init $ eval $(opam env) # Install the REPL and build tools $ opam install utop dune

Running Code

  • utop -- interactive REPL (like Python shell)
  • ocaml -- basic REPL (less fancy)
  • dune -- build system for projects
  • .ml -- OCaml source file extension

Hello World in utop

# print_endline "Hello, CS Theory!";; Hello, CS Theory! - : unit = () # 2 + 3;; - : int = 5 # "formal" ^ " languages";; - : string = "formal languages"

Important: The ;; Terminator

In the REPL, every expression ends with ;; to tell OCaml "evaluate this now." In source files (.ml), you typically do not need ;;.

3 / 22

Basic Types and Values

OCaml's built-in types -- and the compiler figures them out for you

The Primitive Types

TypeExamplesNotes
int42, -7, 0No overflow warning
float3.14, 2.0Separate operators!
booltrue, falseLowercase!
char'a', 'Z'Single quotes
string"hello"Double quotes
unit()Like void

REPL Examples

# 42;; - : int = 42 # 3.14;; - : float = 3.14 # true;; - : bool = true # 'a';; - : char = 'a' # "hello";; - : string = "hello" # ();; - : unit = ()

Gotcha: int vs float arithmetic

OCaml does NOT auto-convert between int and float. Use + for ints, +. for floats. 2 + 3 works, but 2 + 3.0 is a type error! Use float_of_int to convert.

# 2 + 3;; - : int = 5 # 2.0 +. 3.0;; - : float = 5.0 # 2 + 3.0;; (* TYPE ERROR! *) Error: This expression has type float but an expression of type int was expected # float_of_int 2 +. 3.0;; - : float = 5.0
4 / 22

Let Bindings

Naming values -- not assigning variables

Top-Level Bindings

# let x = 5;; val x : int = 5 # let greeting = "hello";; val greeting : string = "hello" # let is_even n = (n mod 2 = 0);; val is_even : int -> bool = <fun> # x + 10;; - : int = 15

Local Bindings with let...in

# let area r = let pi = 3.14159 in let r_sq = r *. r in pi *. r_sq;; val area : float -> float = <fun> # area 5.0;; - : float = 78.53975

Analogy: Mathematical Definitions

In math, when we write "let x = 5," we mean x IS 5. We don't mean "put 5 in the box labeled x." There is no box. There is no mutation. let in OCaml works the same way.

Immutability by Default

Once you bind a name, it cannot change. This is like how in math, once you define a variable in a proof, it stays fixed.

(* You CAN shadow a binding: *) # let x = 5;; val x : int = 5 # let x = 10;; (* new binding, old x is gone *) val x : int = 10 (* But you CANNOT mutate: *) # x = 20;; (* This is equality CHECK, not assignment! *) - : bool = false

Java/Python Gotcha

= is comparison in OCaml, not assignment. let is how you bind names. There is no x = x + 1.

5 / 22

Functions

First-class citizens -- functions are values like any other

Defining Functions

(* Named function *) # let increment x = x + 1;; val increment : int -> int = <fun> (* Multi-parameter function *) # let add x y = x + y;; val add : int -> int -> int = <fun> (* Anonymous function (lambda) *) # (fun x -> x + 1) 5;; - : int = 6 # let double = fun x -> x * 2;; val double : int -> int = <fun>

Partial Application

# let add x y = x + y;; val add : int -> int -> int = <fun> # let add5 = add 5;; val add5 : int -> int = <fun> # add5 3;; - : int = 8

Currying: How Multi-Arg Functions Really Work

add : int -> int -> int This REALLY means: add : int -> (int -> int) "A function that takes an int and returns a function that takes an int and returns an int" ┌─────┐ ┌─────┐ ┌─────┐ │ 5 │───>│ add │───>│ f │ └─────┘ │ │ │ (adds 5│ └─────┘ │to arg) │ └───┬───┘ ┌─────┐ │ ┌─────┐ │ 3 │───┘──>│ 8 │ └─────┘ └─────┘

Key Idea

Every function in OCaml takes exactly one argument. Multi-argument functions are syntactic sugar for curried chains. let add x y = ... is really let add = fun x -> fun y -> ...

6 / 22

Type Inference

OCaml figures out the types so you don't have to write them

The Compiler as Detective

# let mystery x y = if x > 0 then y ^ "!" else y;; (* OCaml deduces: x > 0 --> x must be int y ^ "!" --> y must be string result --> string *) val mystery : int -> string -> string = <fun>
# let pick_first (a, b) = a;; val pick_first : 'a * 'b -> 'a = <fun> (* 'a and 'b are TYPE VARIABLES This works for ANY types! pick_first (1, "hi") --> 1 pick_first (true, 42) --> true *)

Polymorphism (Generics)

# let identity x = x;; val identity : 'a -> 'a = <fun> (* 'a means "any type" -- like Java's <T> but automatic *) # identity 42;; - : int = 42 # identity "hello";; - : string = "hello"

Analogy: Detective Work

Type inference is like solving a mystery. The compiler sees clues: x + 1 means x is an int. y ^ "!" means y is a string. It pieces together evidence to determine every type, without you writing a single annotation.

Hindley-Milner Type System

OCaml uses the Hindley-Milner type inference algorithm. It can infer the most general type for any expression. This is a real CS theory result -- the algorithm is based on unification, the same technique used in logic programming and automated theorem proving.

You CAN Add Type Annotations

If you want to be explicit (or get better error messages):

# let add (x : int) (y : int) : int = x + y;; val add : int -> int -> int = <fun>
7 / 22

If-Then-Else as Expressions

Everything in OCaml is an expression that returns a value

Expressions, Not Statements

(* if-then-else RETURNS a value *) # let abs x = if x >= 0 then x else -x;; val abs : int -> int = <fun> # let max a b = if a > b then a else b;; val max : 'a -> 'a -> 'a = <fun> (* You can use it inline *) # let msg = "You " ^ (if true then "win" else "lose");; val msg : string = "You win"
(* Nested if-then-else *) # let classify n = if n > 0 then "positive" else if n < 0 then "negative" else "zero";; val classify : int -> string = <fun>

Key Idea: Both Branches Must Have the Same Type

Since if-then-else is an expression that produces a value, both branches must produce the same type. This is like how a mathematical piecewise function must have a consistent codomain.

Common Error: Type Mismatch

# if true then 42 else "hello";; Error: This expression has type string but an expression of type int was expected (* "then" branch is int, "else" is string -- OCaml says NO! *)

Gotcha: if Without else

(* Only valid when the result is unit *) # if true then print_endline "hi";; hi - : unit = () (* This FAILS -- what would the else return? *) # if true then 42;; Error: This expression has type int but an expression of type unit was expected
8 / 22

Recursion

No loops needed -- recursion is the way

The rec Keyword

(* MUST use "let rec" for recursion *) # let rec factorial n = if n <= 0 then 1 else n * factorial (n - 1);; val factorial : int -> int = <fun> # factorial 5;; - : int = 120
(* Fibonacci *) # let rec fib n = if n <= 1 then n else fib (n-1) + fib (n-2);; val fib : int -> int = <fun> # fib 10;; - : int = 55

Tail Recursion (Optimized)

# let factorial_tail n = let rec aux acc n = if n <= 0 then acc else aux (acc * n) (n - 1) in aux 1 n;; val factorial_tail : int -> int = <fun>
factorial 4 ├── 4 * factorial 3 │ ├── 3 * factorial 2 │ │ ├── 2 * factorial 1 │ │ │ ├── 1 * factorial 0 │ │ │ │ └── 1 │ │ │ └── 1 * 1 = 1 │ │ └── 2 * 1 = 2 │ └── 3 * 2 = 6 └── 4 * 6 = 24

Gotcha: Forgetting rec

If you write let factorial n = ... factorial (n-1) without rec, OCaml will look for a previously defined function called factorial, not the one you're defining. Always use let rec for recursive functions!

Analogy: Recursion = Induction

Recursive functions mirror structural induction proofs from theory class. Base case = base case. Recursive call = inductive step. If you can write an inductive proof, you can write a recursive function.

9 / 22

Tuples and Records

Grouping data together

Tuples -- Quick Grouping

# (1, "hello", true);; - : int * string * bool = (1, "hello", true) # let point = (3.0, 4.0);; val point : float * float = (3.0, 4.0) (* Access via pattern matching *) # let (x, y) = point;; val x : float = 3. val y : float = 4. # let distance (x1,y1) (x2,y2) = let dx = x2 -. x1 in let dy = y2 -. y1 in sqrt (dx *. dx +. dy *. dy);; val distance : float * float -> float * float -> float = <fun> # distance (0.0, 0.0) (3.0, 4.0);; - : float = 5.

Records -- Named Fields

(* Define a record type first *) # type person = { name : string; age : int; };; type person = { name : string; age : int; } (* Create a record *) # let alice = { name = "Alice"; age = 20 };; val alice : person = {name = "Alice"; age = 20} (* Access fields *) # alice.name;; - : string = "Alice" (* Pattern match on records *) # let greet { name; age } = Printf.printf "%s is %d\n" name age;; val greet : person -> unit = <fun>

Key Idea: The * in Tuple Types

int * string * bool uses * because tuples are Cartesian products of types -- the same cross-product from set theory!

10 / 22

Lists

The fundamental data structure of functional programming

Creating Lists

# [1; 2; 3];; - : int list = [1; 2; 3] # ["hello"; "world"];; - : string list = ["hello"; "world"] # [];; - : 'a list = [] (* empty list *) (* :: is "cons" -- prepend an element *) # 1 :: [2; 3];; - : int list = [1; 2; 3] # 1 :: 2 :: 3 :: [];; - : int list = [1; 2; 3] (* @ is append -- concatenate lists *) # [1; 2] @ [3; 4];; - : int list = [1; 2; 3; 4]

Gotcha: Semicolons, Not Commas!

[1; 2; 3] is a list of three ints. [1, 2, 3] is a list of ONE tuple (1,2,3). This trips up everyone coming from Python/Java.

Lists are Linked Lists

[1; 2; 3] is really: ┌───┬───┐ ┌───┬───┐ ┌───┬───┐ │ 1 │ ─┼──>│ 2 │ ─┼──>│ 3 │ / │ └───┴───┘ └───┴───┘ └───┴───┘ 1 :: (2 :: (3 :: [])) cons is O(1), append is O(n) head is O(1), indexing is O(n)

Key Idea: All Elements Must Be the Same Type

[1; "hello"; true] is a type error. Unlike Python lists, OCaml lists are homogeneous. Think of it as a formal language: the list type is the Kleene star of a single alphabet symbol.

(* List functions *) # List.length [1; 2; 3];; - : int = 3 # List.hd [1; 2; 3];; - : int = 1 # List.tl [1; 2; 3];; - : int list = [2; 3] # List.rev [1; 2; 3];; - : int list = [3; 2; 1]
11 / 22

Pattern Matching (Part 1)

The most powerful feature in OCaml -- your new best friend

Basic Syntax

# let describe_number n = match n with | 0 -> "zero" | 1 -> "one" | 2 -> "two" | _ -> "many";; val describe_number : int -> string = <fun> # describe_number 1;; - : string = "one" # describe_number 99;; - : string = "many"

Matching on Tuples

# let and_gate a b = match (a, b) with | (true, true) -> true | (true, false) -> false | (false, true) -> false | (false, false) -> false;; val and_gate : bool -> bool -> bool = <fun> (* Shorter with wildcards: *) # let and_gate a b = match (a, b) with | (true, true) -> true | _ -> false;;

Analogy: DFA Transition Table!

Pattern matching is structurally identical to a DFA's transition function δ(q, a):

DFA transition table: OCaml match: ┌───────┬───┬───┐ │ State │ 0 │ 1 │ let delta state symbol = ├───────┼───┼───┤ match (state, symbol) with │ q0 │q0 │q1 │ | (Q0, '0') -> Q0 │ q1 │q2 │q0 │ | (Q0, '1') -> Q1 │ q2 │q1 │q2 │ | (Q1, '0') -> Q2 └───────┴───┴───┘ | (Q1, '1') -> Q0 | (Q2, '0') -> Q1 Same structure! | (Q2, '1') -> Q2

Exhaustiveness Checking

OCaml warns you if your patterns don't cover all cases. This is like checking that your DFA transition function is total -- every (state, symbol) pair must have a transition!

# let bad n = match n with | 0 -> "zero" | 1 -> "one";; Warning: this pattern-matching is not exhaustive.
12 / 22

Pattern Matching (Part 2)

Matching on lists -- recursive list processing

List Patterns

# let describe_list lst = match lst with | [] -> "empty" | [x] -> "one element" | [x; y] -> "two elements" | x :: rest -> "many elements";; val describe_list : 'a list -> string = <fun> (* x :: rest destructures the list: x = first element (head) rest = remaining list (tail) *)

Recursive List Processing

# let rec length lst = match lst with | [] -> 0 | _ :: tl -> 1 + length tl;; val length : 'a list -> int = <fun> # let rec sum lst = match lst with | [] -> 0 | x :: tl -> x + sum tl;; val sum : int list -> int = <fun> # sum [1; 2; 3; 4];; - : int = 10

Building Classic Functions

# let rec map f lst = match lst with | [] -> [] | x :: tl -> (f x) :: (map f tl);; val map : ('a -> 'b) -> 'a list -> 'b list = <fun> # map (fun x -> x * 2) [1; 2; 3];; - : int list = [2; 4; 6] # let rec filter pred lst = match lst with | [] -> [] | x :: tl -> if pred x then x :: filter pred tl else filter pred tl;; val filter : ('a -> bool) -> 'a list -> 'a list = <fun> # filter (fun x -> x > 2) [1; 2; 3; 4];; - : int list = [3; 4]

The Pattern (pun intended)

Every recursive list function follows the same template: base case for [], recursive case splitting into head :: tail. This mirrors structural induction on list length!

13 / 22

Algebraic Data Types (Variants)

Custom types with constructors -- the heart of OCaml

Defining Variant Types

# type shape = | Circle of float | Rectangle of float * float | Triangle of float * float * float;; type shape = Circle of float | Rectangle of float * float | Triangle of float * float * float # let s1 = Circle 5.0;; val s1 : shape = Circle 5. # let s2 = Rectangle (3.0, 4.0);; val s2 : shape = Rectangle (3., 4.)

Using Variants with Pattern Matching

# let area s = match s with | Circle r -> 3.14159 *. r *. r | Rectangle (w, h) -> w *. h | Triangle (a, b, c) -> let s = (a +. b +. c) /. 2.0 in sqrt (s *.(s-.a)*.(s-.b)*.(s-.c));; val area : shape -> float = <fun> # area (Circle 5.0);; - : float = 78.53975

Analogy: Grammar Productions!

Each variant constructor is like a production rule in a grammar:

Grammar: OCaml: ────────── ────── shape -> Circle float type shape = | Rect float float | Circle of float | Tri float float | Rectangle of float * float float | Triangle of float * float * float Each "|" is an alternative production. The "of ..." is the right-hand side specifying what data the constructor carries.

Key Idea: Sum Types

Variants are called sum types because a shape is a Circle OR a Rectangle OR a Triangle. The total possibilities are the SUM of all constructors. (Tuples are product types -- AND.) This is algebraic data types = algebra on types!

(* Simple enum-style variant *) # type color = Red | Green | Blue;; # type day = Mon|Tue|Wed|Thu|Fri|Sat|Sun;;
14 / 22

Recursive Types

Types that refer to themselves -- just like recursive grammars

Binary Tree

# type tree = | Leaf | Node of tree * int * tree;; # let my_tree = Node ( Node (Leaf, 1, Leaf), 3, Node (Leaf, 5, Leaf) );; (* 3 / \ 1 5 / \ / \ . . . . *)
# let rec tree_sum t = match t with | Leaf -> 0 | Node (left, v, right) -> tree_sum left + v + tree_sum right;; val tree_sum : tree -> int = <fun> # tree_sum my_tree;; - : int = 9

Arithmetic Expression AST

# type expr = | Num of int | Add of expr * expr | Mul of expr * expr;; (* This IS the CFG from class! E -> n | E + E | E * E *) # let e = Add (Num 1, Mul (Num 2, Num 3));; (* Represents: 1 + (2 * 3) *)
# let rec eval e = match e with | Num n -> n | Add (l, r) -> eval l + eval r | Mul (l, r) -> eval l * eval r;; val eval : expr -> int = <fun> # eval (Add (Num 1, Mul (Num 2, Num 3)));; - : int = 7

Key Idea: Types = Grammars, Values = Parse Trees

The type expr definition IS a context-free grammar. Each value of type expr IS a parse tree. The eval function IS a recursive descent evaluator walking the parse tree. This is the exact same structure from your theory course!

15 / 22

The Option Type

Safe handling of "maybe there's no value" -- no null pointer exceptions

Definition

(* Built into OCaml: *) type 'a option = | None | Some of 'a # Some 42;; - : int option = Some 42 # None;; - : 'a option = None # Some "hello";; - : string option = Some "hello"

Using Options

# let safe_div a b = if b = 0 then None else Some (a / b);; val safe_div : int -> int -> int option = <fun> # safe_div 10 3;; - : int option = Some 3 # safe_div 10 0;; - : int option = None

Pattern Matching on Options

# let describe_result opt = match opt with | None -> "no result" | Some v -> "got: " ^ string_of_int v;; val describe_result : int option -> string = <fun> (* Find in a list *) # let rec find_first pred lst = match lst with | [] -> None | x :: tl -> if pred x then Some x else find_first pred tl;; val find_first : ('a -> bool) -> 'a list -> 'a option = <fun> # find_first (fun x -> x > 3) [1;2;3;4;5];; - : int option = Some 4 # find_first (fun x -> x > 10) [1;2;3];; - : int option = None

Analogy: Java's null vs OCaml's Option

In Java, any reference can be null and you only find out at runtime (NullPointerException). In OCaml, if a value might be absent, the type says option, and the compiler forces you to handle the None case. It's like the difference between an informal proof ("trust me, it's not null") and a formal proof (the type system guarantees it).

16 / 22

Higher-Order Functions

Functions that take functions as arguments or return them

The Big Three: map, filter, fold

(* map: transform every element *) # List.map (fun x -> x * x) [1;2;3;4];; - : int list = [1; 4; 9; 16] (* filter: keep elements matching predicate *) # List.filter (fun x -> x mod 2 = 0) [1;2;3;4];; - : int list = [2; 4] (* fold_left: accumulate a result *) # List.fold_left (fun acc x -> acc + x) 0 [1;2;3;4];; - : int = 10

Function Composition

# let compose f g x = f (g x);; val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun> # let double_then_add1 = compose (fun x -> x+1) (fun x -> x*2);; val double_then_add1 : int -> int = <fun> # double_then_add1 5;; - : int = 11 (* 5*2=10, 10+1=11 *)

Type Signatures Tell the Story

List.map : ('a -> 'b) -> 'a list -> 'b list ──────── ─────── ─────── function input output to apply list list List.filter : ('a -> bool) -> 'a list -> 'a list ────────── ─────── ─────── predicate input filtered (keep if list list true) List.fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a ────────────── ─── ─────── ── accumulator fn init input final value list result

Key Idea

Higher-order functions let you separate the what from the how. map says "transform each element" -- you just provide the transformation. This is abstraction at its finest, and it's why functional code is often shorter than imperative code.

17 / 22

Map and Filter Visualized

Data pipelines -- transform and select

Map: Transform Every Element

List.map (fun x -> x * x) [1; 2; 3; 4; 5] ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ input list └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │ │ │ │ │ v v v v v [x*x] [x*x] [x*x] [x*x] [x*x] apply f to each │ │ │ │ │ v v v v v ┌───┐ ┌───┐ ┌───┐ ┌────┐ ┌────┐ │ 1 │ │ 4 │ │ 9 │ │ 16 │ │ 25 │ output list └───┘ └───┘ └───┘ └────┘ └────┘

Filter: Keep Only Matching Elements

List.filter (fun x -> x mod 2 = 0) [1; 2; 3; 4; 5; 6] ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ input list └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │ │ │ │ │ │ even? even? even? even? even? even? test predicate │ │ │ │ │ │ NO YES NO YES NO YES x │ x │ x │ drop / keep v v v ┌───┐ ┌───┐ ┌───┐ │ 2 │ │ 4 │ │ 6 │ output list └───┘ └───┘ └───┘

Chaining: The Pipeline Pattern

You can chain map and filter to build data processing pipelines. This is just function composition!

(* "Get the squares of all even numbers in the list" *) # [1;2;3;4;5;6;7;8;9;10] |> List.filter (fun x -> x mod 2 = 0) (* keep evens: [2;4;6;8;10] *) |> List.map (fun x -> x * x);; (* square: [4;16;36;64;100] *) - : int list = [4; 16; 36; 64; 100] (* |> is the "pipe" operator: x |> f means f x *)
18 / 22

Fold: The Universal List Function

Every list function can be written as a fold

fold_left: Accumulate Left to Right

List.fold_left f init [a; b; c] Computes: f (f (f init a) b) c init │ v ┌──────┐ │ f a│──> result1 └──────┘ │ v ┌──────┐ │ f b│──> result2 └──────┘ │ v ┌──────┐ │ f c│──> FINAL └──────┘
Example: sum via fold_left fold_left (+) 0 [1; 2; 3] Step 1: 0 + 1 = 1 Step 2: 1 + 2 = 3 Step 3: 3 + 3 = 6 --> result: 6

Everything is a Fold!

(* Sum *) # let sum lst = List.fold_left (+) 0 lst;; (* Length *) # let length lst = List.fold_left (fun acc _ -> acc + 1) 0 lst;; (* Reverse *) # let rev lst = List.fold_left (fun acc x -> x :: acc) [] lst;; (* Map via fold *) # let map f lst = List.fold_right (fun x acc -> f x :: acc) lst [];; (* Filter via fold *) # let filter pred lst = List.fold_right (fun x acc -> if pred x then x :: acc else acc) lst [];; (* Flatten *) # let flatten lst = List.fold_right (@) lst [];;

Key Idea

fold is the universal list eliminator. Any function that consumes a list and produces a single result can be expressed as a fold. In theory terms: fold captures the catamorphism over the list type -- the unique function determined by the algebra of its constructors ([] and ::).

19 / 22

Connections to Theory

A complete DFA simulator in OCaml -- tying it all together

DFA: Formal Definition

M = (Q, Σ, δ, q0, F) Example: accepts strings ending in "ab" Q = {q0, q1, q2} Σ = {a, b} q0 = q0 F = {q2} Transition table: ┌───────┬────┬────┐ │ state │ a │ b │ ├───────┼────┼────┤ │ q0 │ q1 │ q0 │ │ q1 │ q1 │ q2 │ │ q2 │ q1 │ q0 │ └───────┴────┴────┘

DFA Simulator in OCaml

(* Define states *) type state = Q0 | Q1 | Q2 (* Transition function -- pattern matching! *) let delta state symbol = match (state, symbol) with | (Q0, 'a') -> Q1 | (Q0, 'b') -> Q0 | (Q1, 'a') -> Q1 | (Q1, 'b') -> Q2 | (Q2, 'a') -> Q1 | (Q2, 'b') -> Q0 | _ -> Q0 (* catch-all *) (* Accept states *) let is_accepting state = match state with | Q2 -> true | _ -> false
(* Run the DFA on an input string *) let run_dfa input = let chars = List.init (String.length input) (String.get input) in (* string -> char list *) let final_state = List.fold_left delta Q0 chars in (* process all symbols *) is_accepting final_state (* check acceptance *) (* Test it! *) # run_dfa "ab";; - : bool = true (* ends in "ab" *) # run_dfa "aab";; - : bool = true (* ends in "ab" *) # run_dfa "abb";; - : bool = false (* ends in "b", not "ab" *) # run_dfa "bababab";; - : bool = true (* ends in "ab" *) # run_dfa "";; - : bool = false (* empty string *)

Key Idea: The Code IS the Math

delta is literally δ. fold_left delta Q0 chars is literally δ*(q0, w). is_accepting final_state is literally "is δ*(q0, w) in F?" The OCaml code maps 1:1 to the formal definition. This is why OCaml is the natural language for automata theory.

20 / 22

Practical Tips & Common Errors

Java/Python habits that will trip you up in OCaml

Common Errors and Fixes

1. Forgetting rec

let f x = ... f x --> "Unbound value f"
Fix: let rec f x = ...

2. Semicolons vs Double-Semicolons

;; = end of expression in REPL only
; = sequence two unit expressions
In .ml files, you rarely need ;;

3. Type Mismatch Errors

2 + 3.0 --> Error! Use 2.0 +. 3.0
if x then 1 else "no" --> Error! Branches must match.

4. Parentheses for Function Arguments

f g x means "call f with two args: g and x"
f (g x) means "call g on x, pass result to f"

Java/Python to OCaml Translation

Java/Python OCaml ─────────── ───── int x = 5; let x = 5 x = x + 1; (* impossible! use recursion *) for/while loop let rec ... (recursion) null None (option type) x == y x = y x != y x <> y arr[i] List.nth lst i arr.length List.length lst return val (just write the expression) void unit System.out.println print_endline ArrayList list (but immutable) class Foo { ... } type foo = ...

Golden Rules

  • No mutation: use recursion, not loops
  • No return: the last expression IS the result
  • No null: use option type
  • Read the types: they tell you everything
  • When in doubt, pattern match
21 / 22

Summary & Cheat Sheet

Quick reference for everything you need

Syntax Quick Reference

(* Values and bindings *) let x = 42 let f x y = x + y let f = fun x -> fun y -> x + y (* Local binding *) let x = 5 in x + 1 (* Recursion *) let rec f n = if n=0 then 1 else n * f(n-1) (* Pattern matching *) match x with | pattern1 -> expr1 | pattern2 -> expr2 | _ -> default (* Type definitions *) type color = Red | Green | Blue type shape = Circle of float | Rect of float * float type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree (* Records *) type person = { name: string; age: int } let p = { name = "Alice"; age = 20 } (* Option *) type 'a option = None | Some of 'a

Common List Operations

[] (* empty list *) x :: lst (* prepend *) lst1 @ lst2 (* append *) List.length lst (* length *) List.hd lst (* first element *) List.tl lst (* rest of list *) List.rev lst (* reverse *) List.map f lst (* transform each *) List.filter p lst (* keep matching *) List.fold_left f i l (* accumulate L->R *) List.fold_right f l i (* accumulate R->L *) List.mem x lst (* membership check *) List.nth lst i (* index access *) List.sort cmp lst (* sort *)

Theory-to-OCaml Mental Model

Theory OCaml ────── ───── δ(q, a) = q' match (q, a) with ... δ*(q0, w) fold_left delta q0 w CFG production variant constructor Parse tree recursive data type Language L bool-returning function w ∈ L accepts w = true

You're Ready!

You now have the OCaml toolkit to implement DFAs, NFAs, CFGs, parsers, and evaluators for your theory course. The key insight: the code mirrors the math. If you understand the formal definition, you can write the OCaml. Start with the types, then pattern match your way through.

22

We'd Love Your Feedback!

Help us improve these slides -- your responses go directly to a Google Sheet

eCampus better These are better

About these slides

This slide deck was generated with the help of Claude (Anthropic's AI assistant) and reviewed by your instructor. We're curious what you think about AI-assisted teaching materials!

23