← the guideThe Systems Age

§ 02 — Eras · 1973

ML

types inferred, not written — safety, silently

Left: era context, features, and the 101 hello. Right: 3 core concepts, 3 deeper 201 lessons, the factorial specimen, and where ML lives today — each column scrolls on its own.

creators
Robin Milner, Edinburgh, 1973
status
alive through descendants · SML, OCaml, F#, Rust, Haskell
lineage
→ Standard ML→ Haskell

The problem it solves

Proof tactics had to be correct before they ran. Milner made the compiler derive the types — so errors surfaced before execution.

The problem it carries

An academic home and a small ecosystem — for decades, the best language you weren’t allowed to deploy.

Key features

type inference
Types are deduced rather than declared, and the compiler proves the program well-typed.
pattern matching
Functions are defined by cases on the shape of data; a missing case is reported, not defaulted.
polymorphism
One definition serves every type that fits — generics without a single annotation.
the value restriction
The discipline that keeps inference sound once references enter the picture.

101 — hello, world

Session one is the smallest program that still counts as speaking ML: what every tutorial prints first, in the dialect that mattered when the language arrived (1973).

the first program1973
print "Hello, world\n";

101First contact — hello world and 3 ideas that define how ML reads. 201 — 3 lessons on mechanisms you meet after the factorial specimen: the sharp edges practitioners actually use.

101 — core concepts

3 fragments, each naming a habit of mind in ML. Read the note, then the code — together they are the 101 in one sitting.

01 — type inference

No annotations anywhere; the compiler derives the type — and rejects what would break it.

fun len []      = 0
  | len (_::xs) = 1 + len xs
(* val len = fn : 'a list -> int *)

02 — pattern matching

Define a function by cases on the shape of data; missing a case is a warning.

fun sum []      = 0
  | sum (x::xs) = x + sum xs

03 — polymorphism for free

Hindley–Milner inference makes generic code the default, not a feature.

fun id x = x
(* val id = fn : 'a -> 'a *)

201 — going deeper

The 201 assumes you have read the 101 and the specimen. Each lesson is one mechanism — what it is for, how it looks in code, and the trap or payoff that makes ML distinctive at scale.

  1. datatypes and pattern matching
  2. functors
  3. currying and partial application

datatypes and pattern matching

Algebraic types give you variants that carry their own data, and pattern matching turns them into readable, total functions — the standard for “safe” recursion.

datatype 'a tree = Leaf | Node of 'a tree * 'a * 'a tree

fun size Leaf = 1
  | size (Node (l, _, r)) = size l + size r;

functors

Functors are modules with parameters: a dictionary structure once, instantiated for any key type — the ancestor of Rust’s traits and C++’s templates.

signature ORD = type t
signature MAP = functor F (ORD) : sig
  type map
  val empty : map
  val insert : t * map -> map
end

currying and partial application

Every function takes one argument and returns a function; partial application is ordinary, not a trick — which is why ML code often reads as pipelines.

fun add x y = x + y
val inc = add 1          (* still a function *)

The specimen — factorial

The inferred type: you write the function, the language deduces the type — and refuses the program if the two disagree. Milner’s algorithm made safety silent.

the inferred type1973
fun map f []      = []
  | map f (x::xs) = f x :: map f xs;

(* val map = fn : ('a -> 'b) -> 'a list -> 'b list *)

Run it today

installbrew install smlnj

runsml fact.sml

Where it is now

Its type system quietly moved into Scala, Swift, Rust, TypeScript and every modern “algebraic” type you meet.