Interfaces
There is no Any: “any type” is a capability bound
Section titled “There is no Any: “any type” is a capability bound”Makoto has no Any/dynamic/interface{} type — no value that inhabits every type. It is the same call as the deliberately-absent Top (section 2): a thing you can receive but cannot do anything with safely is the lazy-typing that Optional and the error model already closed. The replacement is not a type, it is a bound: “any type that can be used this way.” You never say “receives anything”; you say “receives anything that satisfies X”, and X is exactly the behavior the code will invoke. A type is admitted by what it does, never by mere identity — the whole language works types through behavior.
This is why a printf-shaped call needs no Any. Heterogeneous and typed is a variadic bounded by an interface: fn log(xs: ...Display) takes any number of values of different types, each carrying the one capability the body uses (to_string), erased to an interface value (below). String interpolation is the everyday form of the same thing: "{{ a }} {{ b }}" formats each hole through Display, statically checked, a compile error for a type that does not implement it (section 16) — never a leaked <Object@0x7f…>. The bound is the operation’s requirement made into the parameter’s type, so “receives many of any type” and “knows what it can do with each” are the same declaration, not two.
Implicit satisfaction (Go-style)
Section titled “Implicit satisfaction (Go-style)”It is enough to implement the methods; there is no explicit “implements” declaration:
decl Writeable { fn write(data: []byte) -> error fn read(buf: mut []byte) -> Result[usize, error]}Ownership is part of the contract a method must match. An interface method may mark a parameter @consume (section 5), exactly like a function: decl Sink { fn eat(p: [*]int @consume) } says that whoever calls eat through a Sink hands the pointer over. A call through the interface (s.eat(p), or through a [T + Sink] constraint) is then a move like any direct call to a consuming function: the caller’s obligation to free p is discharged, and using p afterwards is a use-after-consume. A concrete method satisfies an interface method only when their @consume marks agree exactly, parameter by parameter, in both directions. A method that consumes where the interface only borrows (fn (a: A) eat(p: [*]int @consume) against decl Eater { fn eat(p: [*]int) }) would hide the move: a caller holding an Eater keeps using, or frees, the pointer it believes it lent, while the dispatched implementation frees it – a double-free the ownership gate cannot see through the vtable. A method that only borrows where the interface promises to consume leaks what every caller handed over. Either mismatch is a compile error at the point the type would be taken as the interface (let e: Eater = A{...}, a [T + Eater] instantiation), naming the parameter. @consume is the only decorator an interface parameter takes: the others describe a function body, which an interface method does not have. The same discipline as @requires: a per-function contract the dispatched call must honor.
How to implement
Section titled “How to implement”A method is an external function with a receiver (Go style), the only form. The struct keeps data; the methods are external functions:
fn (o: MyStruct) write(...) { ... }There is no function-field-in-the-struct (Zig style): the language only accepts the Go form. Implementing the methods already satisfies Writeable, without declaring “implements”.
Dispatch: vtable for an interface value, monomorphization for a generic constraint
Section titled “Dispatch: vtable for an interface value, monomorphization for a generic constraint”There are two mechanisms, and earlier the doc confused them under “always vtable”. What decides which one applies is whether you have an interface value or a constraint in a generic:
- Interface value (
x: Displayused as a value: storing aDisplayin a variable, in a heterogeneous list) vtable, Go style. The value carries a pair (pointer to the data, pointer to the concrete type’s vtable); the vtable is assembled by the compiler per type, not per instance. The concrete type is erased (type-erased): you call the methods, but you do not recover “this is aList”. Cost: the pair (data, vtable) at the use site, with no per-object overhead, and the struct never carries inline function pointers. - Constraint in a generic (
fn f[T + Display](x: T),map[C[_] + Mappable]) monomorphization, static dispatch. Each concrete type generates a specialized copy (section 10/13) where the type is known, and the method call is direct, without vtable and without erasure. It is necessary when the return mentions the concrete type:mapreturnsC[B], and you can only build a concreteC[B]by knowingCstatically; aMappableerased by vtable would not know which container to build. It is the same thing section 10 already says forSerializable([T + Serializable]resolves at compile time, straight code).
In summary: vtable when you want to forget the concrete type (heterogeneity); monomorphization when you need to remember it (HKT return like C[B], straight code).
Both forms exist for every interface, Serializable included. Its examples above all use the constraint form ([T + Serializable]), because serialization usually knows the concrete type, but that is a habit, not a restriction: fn f(s: Serializable) takes a dynamic interface value (the fat pointer, so a heterogeneous []Serializable is expressible), while fn g[T + Serializable](x: T) is the monomorphized constraint, and the two even compose in one signature (fn x[T + Serializable](y: Serializable) — the first Serializable bounds the generic T, the second types the argument y). The value form works because a boxed type’s serialize is generated (by auto-derivation, section 10) at the point of boxing, where the concrete type is still known, and the vtable merely points at it — the empty capability dispatches no method itself; it marks “this can be serialized”.
A generic method (one that itself introduces a type parameter, fn get[U](x: U) -> U) resolves only through monomorphization, never through a vtable. Given decl Container { fn get[U](x: U) -> U } and a Bag that implements it:
- Through a constraint it works and needs no wrapper:
fn use[T + Container](c: T, y: int) -> int { c.get(y) }. The concrete type is known at the specialization, soUis inferred at each call site and the method is monomorphized like any other generic. This is the ergonomic form — you write[T + Container]and callc.get(y)directly, withUflowing from the argument. - Through an interface value it cannot work, because the vtable would need one slot per possible
U, and the concrete type is erased:let c: Container = Bag{}; c.get(y)has no monomorphized target to dispatch to. The compiler rejects this cleanly (“a generic method cannot be called through an interface value; bound it with a monomorphized constraint[T + Container]instead”), rather than accepting the declaration and failing later with a confusing inference error.
This keeps Makoto monomorphization-only for generic-method dispatch: it disarms the coherence footgun (section 10) and covers the real cases (T + container) without a wrapper. The alternative — dictionary-passing, which would let a heterogeneous []Container dispatch a generic method with a runtime-varying U — is deliberately not adopted; it is recorded as a possible future core change should a concrete need for that heterogeneity appear.