"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
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
Second, the client-facing view — where methods return "another counter of the same kind" — is naturally a
recursive type, because
The 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
Two primitive operations act on such objects — and note there are only these two. Method
invocation self to
The substitution 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
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:
Depth subtyping refines fields — replace a field's type by a subtype, pointwise:
Subtyping is structural here: what matters is the shape of the object, not a declared
class hierarchy. A extends". This is exactly TypeScript's model, which is why the code below works with
no inheritance keyword in sight.
The existential "hide the representation" idea has a direct realisation: a closure over a
local variable. The variable is the private state 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):
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
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).