skip to content

Three teams each add a method to a type they do not own: one reopens the class at runtime, one declares a static extension method, one adds a protocol conformance. Classify the coupling each creates, and explain why Rust forbids the third outright.

level: principalimportance: nice to knowfreq 30%

answer

  1. all three = hidden coupling, differ by blast radius
  2. reopening = global mutable class object, load order wins
  3. extensions = static, import-scoped, never override; real members win
  4. conformance is global -> Swift @retroactive
  5. orphan rule = coherence, paid for with newtypes

basics

~20 s

All three are hidden coupling - behaviour appears on a type from a place that type never mentions. Ruby and Python reopening mutates a globally shared class object, so load order decides the winner; C# and Kotlin extensions are static and import-scoped; Swift retroactive conformance can collide at link time, which is exactly what Rust's orphan rule prevents.

solid answer

~60 s

All three create **hidden coupling**; they differ in blast radius. - **Ruby and Python** reopening mutates a globally shared object at runtime. Two libraries patching the same method silently break each other and the winner depends on load order - global mutable state plus load-order dependence, invisible at the call site. - **JavaScript** is the historical warning: `Array.prototype.x = ...` leaked into every `for...in`, which is why modern patches are non-enumerable via `Object.defineProperty` or keyed by `Symbol`. - **C# and Kotlin** extension methods are resolved statically and only where imported, so nothing global is mutated and no dispatch changes - but they never override, and if the owner later adds a real member with that name the member silently wins on recompile. - **Swift** extensions can add *conformance*, which is global by nature: two modules conforming the same type to the same protocol is a link-time coin flip, so Swift asks for `@retroactive`. - **Rust** refuses it: `impl Trait for Type` requires you own the trait or the type. That buys coherence - one implementation per pair, program-wide - and costs newtype wrappers.

code

swift · 3 lines
swift
// in module A
extension Date: @retroactive Comparable { ... }
// in module B - same type, same protocol, no scoping rule can keep both

go deeper

for a junior

Recognise that all three add behaviour from outside the type and that a reader of the type's source cannot see it.

for a middle

Distinguish runtime mutation from compile-time, import-scoped extension, and say why load order matters for the first.

for a senior

Add conformance as the third case and explain why it cannot be scoped, plus the extension-versus-member precedence hazard on recompile.

for a principal

Set the policy: rank mechanisms by blast radius, forbid published libraries from mutating shared types, and be explicit that coherence guarantees are bought with wrapper boilerplate.

## What the three mechanisms have in common Each lets code in module X change what a type declared in module Y can do, without Y knowing. That is hidden coupling: a reader of Y's source cannot enumerate Y's behaviour, and a reader of a call site cannot tell from the receiver's declared type where the method came from. Beyond that shared property, the three differ enormously in *how far the change reaches* and *when the conflict is discovered*, and that is what an interviewer is testing. ## Runtime reopening: global mutable state that happens to be a class Ruby classes are open: any file may reopen `class String` and add or replace a method, at any time during load. Python does the same by assigning to a class attribute, and JavaScript by assigning to a prototype. The class object is shared, mutable, process-global state, so this is common coupling with an object dressed as a type. Its properties follow directly: two libraries that patch the same method do not conflict loudly, they conflict by load order, and the loser leaves no trace. Behaviour becomes dependent on require order, which is often decided by a dependency resolver rather than by anyone's design. Because the patch site and the call site are in different files with no reference between them, debugging starts with "where on earth did this method come from". JavaScript supplies the historical scar. Adding to `Array.prototype` in the enumerable way made the new property appear in every `for...in` loop over any array anywhere in the page, which broke unrelated libraries en masse. The community response is instructive: patch with `Object.defineProperty` and `enumerable: false`, or key the addition with a `Symbol` so it cannot collide with a string-named property at all. Both are ways of shrinking a global change's visibility after the fact. ## Static extensions: compile-time, import-scoped, non-virtual C# extension methods and Kotlin extension functions look like the same feature and are a fundamentally different one. The compiler rewrites `receiver.Foo()` into a static call `Ext.Foo(receiver)`, chosen from the extensions in scope via `using`/`import`. Nothing is added to the type; nothing is mutated at runtime; another assembly that does not import your namespace sees no change at all. The blast radius is a lexical scope, which is about as small as this feature gets. The costs are precise and worth naming. First, dispatch is static: the extension is selected by the *declared* type at the call site, so it cannot participate in polymorphism and a subtype cannot specialise it. Second, real members win. If the owning library later adds an instance method with a matching signature, that member takes precedence and your call sites silently switch behaviour on the next recompile - a source-compatibility hazard that has bitten real ecosystems when a popular extension name was later added to the standard library. ## Retroactive conformance: local syntax, global meaning Swift extensions can do something the other two cannot: attach a *protocol conformance*. That is qualitatively different from adding a method, because conformance is looked up globally - generic code anywhere asks "does this type conform to this protocol" and must get one answer. If module A conforms `Date` to `Codable`-like protocol P and module B does the same, there is no scoping rule that can keep both, and the winner is decided at link time. Swift's response is `@retroactive`, an explicit marker that you are conforming a type you do not own to a protocol you do not own and accept the risk. Objective-C categories are the untyped ancestor: two categories defining the same selector produce an undefined winner with no diagnostic, which is why prefixing category methods became standard practice. ## Rust: the orphan rule Rust makes coherence a language guarantee: for any (trait, type) pair there is at most one implementation in the entire program. It enforces this with the orphan rule - you may write `impl Trait for Type` only if the trait or the type is local to your crate. That makes the Swift collision unrepresentable rather than merely discouraged, which matters because Rust's generic dispatch, like Swift's, resolves conformance globally. The price is real and should be stated, not glossed: when you genuinely need someone else's trait on someone else's type, you must define a newtype wrapper and re-expose whatever surface you need through it. Rust chose predictable global reasoning over convenience, and the boilerplate is the bill. ## How to choose Rank the mechanisms by blast radius and pick the smallest that does the job. 1. A free function or a method on a type you own - no hidden coupling at all. 2. A newtype or wrapper - explicit at the call site, no effect on anyone else. 3. A statically scoped extension - visible only where imported; acceptable for ergonomics, never for behaviour a caller must not miss. 4. A runtime patch - only for a temporary fix to a library defect, pinned to a version, with a comment naming the upstream issue and a removal condition. And never rely on a patch for correctness in a library you publish, because your consumers inherit the global change and will conflict with each other on your behalf. ## The answer to give Classify all three as hidden coupling, then differentiate by blast radius and discovery time: global mutation discovered by load order; lexical scope discovered at compile time with a precedence hazard; global conformance discovered at link time; and Rust's refusal to allow the third case at all, bought with newtype boilerplate.

  • Why can a C# extension method never override an instance method?
    Because it is not part of the type: the compiler rewrites the call into a static invocation chosen from the extensions in scope, and it only considers extensions when no applicable instance member exists. That gives instance members precedence by construction, so an extension cannot participate in virtual dispatch and cannot be specialised by a subtype. The practical consequence is that if the owning library later adds a matching member, your call sites silently switch to it on recompile.
  • When is a runtime monkey patch defensible?
    As a temporary, pinned workaround for a defect in a dependency you cannot fork or wait on: applied in application code rather than in a published library, narrow enough to be read in one screen, commented with the upstream issue and the condition for removal. It is indefensible in a library you publish, because every consumer inherits the global change and two consumers patching the same method will conflict on your behalf with no diagnostic.
  • What does the orphan rule cost, and how do teams pay it?
    It forbids implementing a foreign trait for a foreign type, so when you need that combination you define a newtype wrapper and forward or re-expose the surface you need through it. The cost is boilerplate and a wrapper type leaking into signatures; the benefit is that any generic code can rely on there being exactly one implementation per trait-and-type pair for the whole program, which is what makes conformance-based dispatch predictable.

Three ways to add a note to a shared library book: write in the margin of the only copy (runtime patch), keep a sticky note in your own reading room (scoped extension), or register your annotation in the global catalogue that every reader consults (conformance). Only the third can be contradicted by a stranger's entry.

saying these in an interview costs you the question

  • Treating monkey patching and extension methods as the same mechanism with different syntax
  • Believing an extension method can override or be overridden
  • Assuming two modules adding the same conformance will produce a compile error everywhere
  • Saying the orphan rule is free, without mentioning newtype boilerplate
  • Shipping a library that patches a shared class and calling it a convenience

context