Skip to content

Rationale · essay 07

Types

rationale.md · 90 lines · 4 min read

The redefinition: integers of arbitrary width, decl unifies struct/enum/interface, structural satisfaction, and two dispatches (vtable vs monomorphization) chosen by need.

A type system does three jobs: describe data, describe contracts, and decide how calls are dispatched. The mainstream languages take a handful of decisions here that Makoto revisits: fixed widths (i32/i64), separate keywords (struct/enum/interface/trait), declared satisfaction (impl Trait for T), and a fixed dispatch strategy.

  • Integers of arbitrary width: iN/uN for any N from 1 to 65535 (u7, i128, u777). byte is u8, bit is u1, unified, the same thing conceptually. The width lives in the type and documents the intent (“this fits in 7 bits”), instead of being an accidental decision inherited from the hardware.
  • decl unifies struct, enum and interface: one keyword, and the body disambiguates (fields struct; variants enum; signatures interface). They are the same act (declaring a type) in the same realm.
  • Structural satisfaction: if a type has the methods the interface requires, it satisfies it. There is no implements/impl. The type does not have to know beforehand which interfaces it will satisfy.
  • Two dispatches, chosen by need: an interface-value vtable (a data+vtable pair, assembled by the compiler per type, erased); a generic constraint (T + Trait, HKT) monomorphization (static specialization). And the strict ruler of constraints: + = implements an interface, : = belongs to a whitelist. Two symbols because they are two different questions.

Why does decl unify three but not union? By the “operation + realm, not form” rule (ch. 01). Struct/enum/interface have bodies of different shape and still fuse, because they are the same verb (declaring a type) in the same realm (the type system). union has a body identical to a struct’s and still stays out, because it is another operation (reinterpreting bytes) in another realm (the unsafe one). Unifying union by surface similarity would be erasing a boundary.

Why structural satisfaction? Because it decouples. The implementation does not have to know the interface to satisfy it: you create a type, and later someone discovers that it serves where the interface was expected. The impl ritual disappears (and the noisy error of forgetting it), and the validation happens at the point of use.

Why two dispatches instead of one? Because each one solves what the other cannot. The vtable serves when the action does not return the concrete type (w.write(...)): a uniform fat pointer, cost at the point of use, zero per-object overhead. But when the result depends on the type, as in map[A,B](xs: C[A]) -> C[B], which abstracts over the constructor C, the vtable is not enough; you have to monomorphize. The ruler: an action that does not return the receiver’s type vtable; a transformation that returns it monomorphization.

Why two constraint symbols? Because one name does not distinguish “belongs to the set Foo” from “implements the interface Foo”. : introduces only a whitelist ({List, Set}, membership); + introduces only an interface (+ Writeable, implementation). Two symbols because they are two axes, and fusing them would reopen the ambiguity that explicitness wants to kill.

  • Fixed widths (i32/i64). They force the capacity decision into the user’s code and do not document intent; i7 says what int hides.
  • union inside decl. Different operation and realm (ch. 01); fusing them would erase the unsafe boundary.
  • Explicit impl Trait for T (Rust). Coupling and ritual; forgetting the impl is a common and noisy error. Structural removes the step.
  • Function pointers inline in the struct (pure Zig). They would inflate each instance; the per-type vtable shares, keeping the object lean.
  • Arbitrary higher-order HKT (Scala/Haskell). Arity ≤ 2 (C[_], C[_,_]) covers most of useful FP; allowing monad transformers would open the abyss of kind inference. Powerful without being a footgun.

The i32 that silently overflows because nobody decided whether it fit. The forgotten impl in Rust and the trait-resolution error that appears three layers of instantiation below, “candidate #3 has wrong return type”. The Zig object that fattens because it carries its own method pointers. Each one is a type decision that leaked cost to where it should not have.

The type says exactly what it is; the width lives in the type. A decl declares data or a contract (the body decides); satisfying is having the methods; the dispatch is a vtable when the action does not return the type, monomorphization when it does.

The unification loads decl with responsibility (the body has to disambiguate three things), and structural satisfaction trades the explicit declaration for validation at the call-site: you do not announce that you satisfy, but you also have no silent surprise, because the compiler checks where the interface is required. And the shallow HKT (arity ≤ 2) leaves out the higher-order transformers, expressiveness consciously sacrificed so as not to import the inference hell.

Next: 08 · Comptime and reflection