Languages disagree about who decides that a concrete type satisfies an abstraction: in Java the type's author declares it, in Go the compiler infers it from method shape, and in Rust a third party may be forbidden from declaring it at all. Compare these models and what each one costs.
answer
- Who declares conformance: the type, the consumer, or either
- Java nominal: intent documented, retrofitting needs an adapter
- Go implicit: consumer-defined interfaces, accidental satisfaction
- var _ io.Reader = (*T)(nil) is a hand-made declaration
- Rust orphan rule: trait or type must be local, newtype otherwise
basics
~20 sNominal (Java, C#): the type's author writes implements, so retrofitting a library type needs a wrapper. Structural (Go, TypeScript): matching method shape is enough, so a consumer can define the abstraction afterwards. Rust traits are nominal but either side may write the impl, and the orphan rule forces a newtype when neither is local.
solid answer
~60 sThree answers to 'who declares conformance', each with a bill. - **Java, C#, Kotlin (nominal)** — the implementing type names the interface. Intent is documented and accidental conformance is impossible; the price is that you cannot make a type you do not own satisfy your abstraction without an adapter class. - **Go (implicit/structural)** — the *consumer* declares the interface and any type with the right methods satisfies it, so abstractions can be introduced over existing packages retroactively. The price is accidental satisfaction: a `String() string` method makes anything a `fmt.Stringer` whether or not it means the same thing, and there is no declaration of intent, hence the `var _ io.Reader = (*T)(nil)` idiom. - **TypeScript (structural, erased)** — same shape-matching at compile time, but nothing survives to runtime, so there is no runtime conformance test. - **Rust (nominal, either side)** — the trait's author or the type's author may write the `impl`; the orphan rule requires one of them to be local, buying coherence at the cost of a newtype wrapper for foreign-foreign pairs. Haskell permits orphan instances with a warning; Scala 3 `given` instances allow two conflicting choices in different scopes.
code
go · 7 lines// package report (mine) - the type below comes from a library I do not own
type Sizer interface { Size() int64 }
func Summarize(s Sizer) string { ... }
// vendor/blob.Blob already has: func (b *Blob) Size() int64
// Summarize(blob) compiles. No declaration was added to blob.Blob.go deeper
Know that some languages require an explicit implements declaration and others infer conformance from the methods present, and give one example of each.
Contrast the two on retrofitting a type you do not own, and mention Go's blank-identifier assertion as the workaround for the missing declaration.
Bring in Rust's orphan rule and coherence, and explain the adapter/newtype cost that both nominal and coherent-nominal models impose.
Frame it as the expression problem's second axis — adding an abstraction over existing types — and decide where an abstraction should live (provider, consumer, or a separate trait crate) given who will need to extend it.
## What 'program to an interface' actually asks Every language lets a client depend on an abstraction rather than a concrete type. What differs — and what determines how far the advice can be taken — is the answer to a narrower question: **who gets to say that a given concrete type satisfies a given abstraction, and when?** The answer decides whether you can apply the advice to code you did not write. ## Nominal conformance: the implementing type opts in Java, C# and Kotlin use *nominal* conformance. `class ArrayList implements List` is a declaration by the author of `ArrayList`. Two consequences follow. First, conformance is intentional. If a type has a `close()` method by coincidence, it is not `AutoCloseable`, and no client can treat it as one. The compiler will not confuse two unrelated abstractions that happen to share a method name, and the declaration is readable documentation. Second, conformance is *closed to third parties*. If a library ships `Money` and you define an abstraction `Serializable`, you cannot make `Money` implement it. Your options are an adapter class that wraps `Money`, a separate function overloaded on `Money`, or convincing the library author. This is one horn of the expression problem: nominal interfaces make it cheap to add a new implementation of an existing abstraction, and expensive to add a new abstraction over existing types. ## Structural conformance: the consumer declares the abstraction Go inverts the ownership. An interface is satisfied implicitly by any type with the required method set, and idiomatic Go declares the interface *in the consuming package*: the function that needs 'something I can read from' declares a one-method interface next to itself. The implementing type never mentions it, may predate it by years and may live in a third-party module. Retrofitting is therefore free, and the abstraction can be exactly as wide as the consumer needs rather than as wide as the provider guessed. The price is that accidental satisfaction is real. Any type with `String() string` is a `fmt.Stringer`, whether that method means 'render for humans' or something unrelated, and a type with a `Read([]byte) (int, error)` method is an `io.Reader` even if its error and short-read semantics differ from the documented contract. The method set is checkable; the behavioural contract is not. Go programmers compensate with the compile-time assertion `var _ io.Reader = (*MyType)(nil)`, which is a hand-written substitute for the declaration that nominal languages force. TypeScript is structural too, but with a further twist: its structural types are erased. Compatibility is checked at compile time and nothing remains at runtime, so there is no equivalent of asking an object whether it implements an interface; you must hand-write a predicate over properties. ## Rust: nominal, but the impl can come from either side Rust's traits look nominal — `impl Display for Money` is an explicit declaration — but the declaration does not have to be written by the type's author. The trait's author may implement their trait for foreign types, and a type's author may implement foreign traits for their type. What is forbidden is a *third* party doing both: the orphan rule requires that either the trait or the type be local to the crate writing the impl. The reason is **coherence**. Rust wants at most one implementation of a given trait for a given type across the entire program, so that `x.to_string()` means the same thing regardless of which crates are linked in. Without the rule, two independent crates could each implement `Display for Money` and the program would either fail to link or behave differently depending on build order. The cost is familiar to every Rust programmer: to implement a foreign trait for a foreign type you wrap it in a local newtype and forward the methods you need — the same adapter the nominal languages force, but required only for the foreign-foreign case. Haskell makes the same trade more loosely: orphan instances are legal but warned about, precisely because they can break global coherence in ways that only appear when modules are combined. Scala 3 goes further and gives up global coherence: `given` instances are selected by lexical scope, so two parts of a program can legitimately use different `Ordering` instances for the same type. That is more expressive — you can order the same type differently in two contexts without a wrapper — and it means the meaning of a call depends on what is in scope at the call site. ## Choosing, in practice In a nominal language, treat the interface as part of the provider's published surface, and expect adapters whenever you want to abstract over types you do not own. In Go, keep interfaces small and declare them where they are consumed; a large interface in the provider package is fighting the model. In Rust and Haskell, decide early whether an abstraction belongs to the trait's side or the type's side, because that determines who can extend it later. And in every language, remember the part no conformance model checks: matching methods is not matching behaviour.
- Go has no 'implements' keyword, yet experienced Go code often contains a line like `var _ io.Reader = (*MyType)(nil)`. Why?Because implicit satisfaction gives no compile-time signal that a type was meant to satisfy an interface. If someone renames or changes the signature of a method, the type silently stops satisfying it, and the error appears at some distant call site or, worse, only where a type assertion fails at runtime. The assignment to a blank identifier forces the compiler to check conformance at the definition, which is a hand-written stand-in for the declaration a nominal language requires.
- What does Rust's orphan rule protect, and what would break without it?It protects coherence: at most one implementation of a trait for a type across the whole program. Without it, two unrelated crates could each implement the same trait for the same type, and the meaning of a method call would depend on which crates happen to be linked in — or the program would fail to build once both appeared in one dependency graph. The cost is that implementing a foreign trait for a foreign type requires a local newtype wrapper. Scala 3 deliberately gives up global coherence, letting scope select among competing instances.
saying these in an interview costs you the question
- Believing Go has some hidden registration step, or that a type must import the interface's package
- Assuming matching method signatures implies matching behavioural contracts
- Treating TypeScript's interfaces as runtime entities you can test for with a type check
- Saying Rust forbids implementing traits for third-party types outright, rather than only foreign-trait-on-foreign-type
- Concluding that structural typing is simply better, ignoring accidental satisfaction and the missing declaration of intent