Objects as a Calculus

"Object" sounds like an implementation detail — a thing with fields and methods that some compiler lays out in memory. But strip away the syntax of class and new and an object is a precise mathematical gadget: a record of functions that share a hidden piece of state. The functions are the methods; the shared state is the object's private representation; and the fact that a method can call other methods on the same object — this.foo() — is what makes an object more than a plain record. This lesson pins that gadget down as a calculus.

Three ingredients from earlier in the course reassemble into "object". Records give the bundle of methods. Existential types hide the representation, delivering encapsulation as a theorem rather than a convention. Recursive types account for self / this, the knot of an object referring to itself. Add subtyping and you get the "an object with more can stand in for an object with less" discipline that makes object-oriented code compose. We will meet Abadi and Cardelli's ς-calculus, the \lambda-calculus's object-oriented cousin, and see why mutable fields make object subtyping surprisingly subtle.

An object is a record of methods over hidden state

Take the humblest object, a counter. It exposes a way to read its value and a way to advance it, while the value itself — the representation R — stays private. Two type-theoretic constructions capture this. First, an existential seals the representation: clients know a counter has some internal type R and a bundle of operations over it, but not what R is:

\mathtt{Counter} \;\cong\; \exists R.\ \big\{\ \mathit{state} : R,\ \ \mathit{get} : R \to \mathtt{Int},\ \ \mathit{inc} : R \to R\ \big\}

Second, the client-facing view — where methods return "another counter of the same kind" — is naturally a recursive type, because \mathit{inc} yields a \mathtt{Counter} again:

\mathtt{Counter} \;\cong\; \mu C.\ \big\{\ \mathit{get} : \mathtt{Int},\ \ \mathit{inc} : C\ \big\}

The \mu C ("the type C such that …") ties the knot: C appears on both sides, which is exactly the self-reference that distinguishes an object from a flat record. Reveal how the pieces fit — hidden state within, methods around it, a self arrow closing the loop, and a subtype relation to a smaller object:

self, and the Abadi–Cardelli ς-calculus

In the pure object calculus of Abadi and Cardelli there are no functions and no classes — only objects, built from methods, each of which binds a name for self. Write \varsigma(s)\,b for "a method whose body is b, with s bound to the enclosing object." A counter object is literally:

o \;=\; \big[\ \mathit{n} = \varsigma(s)\,0,\quad \mathit{get} = \varsigma(s)\,s.n,\quad \mathit{inc} = \varsigma(s)\,(s.n \Leftarrow s.get + 1)\ \big]

Two primitive operations act on such objects — and note there are only these two. Method invocation o.l runs the method labelled l, binding self to o itself. Method update o.l \Leftarrow \varsigma(s)\,b produces a new object identical to o except that method l is replaced. Their reduction rules make the self-binding explicit:

o.l_j \longrightarrow b_j\{\,s := o\,\} \qquad\qquad (o.l_j \Leftarrow \varsigma(s)\,b) \longrightarrow \big[\dots,\ l_j = \varsigma(s)\,b,\ \dots\big]

The substitution b_j\{s := o\} is the whole magic of this: invoking a method plugs the entire object in for self, so the method can call its siblings and even re-invoke itself. Remarkably, this two-operator calculus is Turing-complete and can encode the \lambda-calculus — objects are not sugar over functions; each can simulate the other.

Subtyping: more is less

Objects compose through subtyping: an object offering more can be used wherever one offering less is expected. Two orthogonal rules govern object (record) types. Width subtyping drops fields — a richer object is a subtype of a poorer one:

\{\, l_i : T_i \ ^{i \in 1..n+k}\, \} \;<:\; \{\, l_i : T_i \ ^{i \in 1..n}\, \}

Depth subtyping refines fields — replace a field's type by a subtype, pointwise:

\dfrac{\text{for each } i,\quad S_i <: T_i}{\{\, l_i : S_i\, \} \;<:\; \{\, l_i : T_i\, \}}

Subtyping is structural here: what matters is the shape of the object, not a declared class hierarchy. A \mathtt{ColorPoint} is usable as a \mathtt{Point} because it has the right fields, whether or not anyone declared "extends". This is exactly TypeScript's model, which is why the code below works with no inheritance keyword in sight.

Encoding an object with closures over hidden state

The existential "hide the representation" idea has a direct realisation: a closure over a local variable. The variable is the private state R; the returned record of functions are the methods; and because they all close over the same variable, they share it. Self-reference is a method calling a sibling through a self binding. Run this — note the counter's state is genuinely inaccessible from outside, and a Counter is accepted where a smaller Readable is wanted (width subtyping):

// The client-facing recursive type: inc returns a Counter again (μC. {...}). interface Counter { get(): number; inc(): void; incBy(k: number): void; // defined in terms of inc via `self` } function makeCounter(): Counter { let count = 0; // HIDDEN state R — no client can name it (∃R) const self: Counter = { get: () => count, inc: () => { count += 1; }, // self-reference: incBy calls the object's OWN inc method (this.inc) incBy: (k: number) => { for (let i = 0; i < k; i++) self.inc(); }, }; return self; } const c = makeCounter(); c.inc(); c.incBy(4); console.log("count =", c.get()); // 5 — inc + incBy(4), all over one hidden `count` // Structural WIDTH subtyping: Counter <: Readable (Counter has get() and more). interface Readable { get(): number; } function report(r: Readable): void { console.log("read via supertype =", r.get()); } report(c); // a Counter is accepted where a Readable is expected // The state is truly encapsulated: there is no `c.count` to touch. console.log("has own 'count' field? ", Object.prototype.hasOwnProperty.call(c, "count")); // false

Nothing here mentions class. The object is a record of closures; encapsulation is the closure hiding count; this is the self binding; and "a Counter is a Readable" is structural width subtyping, checked by shape. That is the whole object model, assembled from parts you already had.

There is an old koan: "Objects are a poor man's closures; closures are a poor man's objects." Both bundle behaviour with private state — a closure hides its captured variables, an object hides its fields — and each can encode the other. A closure is essentially a one-method object; an object is a record of closures over shared state, exactly as the code above shows. The difference is one of emphasis: objects foreground many operations over one hidden state and lean on subtyping; closures foreground one operation and lean on higher-order functions. Where they genuinely diverge is the hard binary-method problem: a method like equals(other: Self) that takes another object of the same type resists clean subtyping, because a subtype's equals wants a narrower argument than the supertype's — contravariance fighting the "self-type." Whole research languages exist to tame that one wrinkle.

Depth subtyping is unsound for mutable fields. It is tempting to say "a \{p : \mathtt{Ref\ Point3D}\} is a \{p : \mathtt{Ref\ Point2D}\}, since \mathtt{Point3D} <: \mathtt{Point2D}." It is not. A mutable reference is both read and written, so it is invariant: \mathtt{Ref}\,S <: \mathtt{Ref}\,T requires S = T, not merely S <: T. Allow the covariant version and you could alias the cell at the wider type and store a plain 2-D point into a slot the other alias still reads as 3-D — the same aliasing hole from the references lesson. Java made exactly this mistake with covariant arrays: String[] is treated as a subtype of Object[], so storing an Integer into a String[] aliased as Object[] type-checks but throws ArrayStoreException at runtime. Immutable (read-only) fields are safely covariant; mutable ones never are.

And do not conflate subtyping with subclassing. Subtyping is a relation on types (structural "has the right shape"); subclassing is a mechanism for code reuse (inheritance). A language can have either without the other — TypeScript's subtyping is structural and needs no extends; and an inherited class is not automatically a behavioural subtype (the Liskov substitution principle is a discipline, not a guarantee the compiler gives you).