skip to content

A method takes a boolean parameter that selects which of two behaviours it performs. Name the coupling that creates, and explain how argument labels (Swift, Smalltalk, Python keyword-only parameters) and closed sum types (Rust, Kotlin, Swift enums) each change the diagnosis.

level: seniorimportance: should knowfreq 55%

answer

  1. control = argument steers the branch, not the data
  2. labels fix reading, not coupling
  3. options object = stamp coupling + silent typo
  4. TS excess-property check: literals only
  5. sum type = closed vocabulary, exhaustive check; defmulti removes the branch

basics

~20 s

Control coupling: the caller passes a value whose only job is to steer the callee's internal branch, so the caller must know branches it should not. Labels make the flag readable but leave the coupling; only turning the choice into a dispatched, exhaustively checked variant removes it.

solid answer

~60 s

**Control coupling** - the argument carries no data, only a decision about the callee's control flow. - **Smalltalk** keyword messages label every argument in the selector itself, so the call site reads clearly - but the branch still lives inside the method. Labels fix legibility, not coupling. - **Swift** makes external argument labels mandatory by default, and **Python** has keyword-only parameters (`def sort(*, reverse)`), so a bare `true` becomes unidiomatic or impossible. Same verdict: the caller still knows the callee's branch. - **JavaScript** options objects convert control coupling into *stamp* coupling: you now depend on an object shape, and `{revrse: true}` silently takes the default. TypeScript's excess-property check catches that for object literals only, not for a variable passed in. - **Rust, Kotlin and Swift** sum types make the choice a closed, exhaustively checked vocabulary - adding a third mode becomes a compile error at every match site. - **Clojure's `defmulti`** and CLOS generic functions move the branch out of the callee entirely; new modes are added without editing the original function.

code

typescript · 6 lines
typescript
type Opts = { inline?: boolean };
declare function render(n: Node, o: Opts): void;

render(n, { inlnie: true });        // error: excess property
const o = { inlnie: true };
render(n, o);                        // no error - silently uses the default

go deeper

for a junior

Name control coupling and give the symptom: the callee starts with a branch on the parameter and the caller has to know what the value selects.

for a middle

Separate readability fixes (argument labels, keyword-only parameters) from structural fixes (enums, separate methods), and say why the first does not change the dependency.

for a senior

Compare the failure modes concretely - options-object typos in JavaScript versus exhaustiveness checking in Rust or Kotlin - and know where TypeScript's excess-property check does and does not apply.

for a principal

Discuss it as a vocabulary decision: whether new modes should force a compile error at every site (closed sum type) or be addable without touching existing code (multimethods), and which direction of the expression problem your system needs.

## The classification Control coupling exists when one unit passes another a value whose purpose is to select behaviour rather than to supply data. The tell is that the caller must know something about the callee's *implementation* - that it has two branches, and which one each value selects. If you change the callee from two branches to three, or swap the meaning of the flag, every caller is affected even though the callee's data contract never changed. A second, related tell: the callee's body starts with a branch on the parameter, and the two arms share almost nothing. That is two functions in a trench coat, and the flag is the coat. ## What argument labels do and do not fix A large family of languages attacks the *readability* symptom. **Smalltalk** has no positional arguments at all: a message is a selector with interleaved keywords, `copyFrom: 1 to: 5`, so every argument is named at the call site by construction. **Swift** carries the idea into a curly-brace language with mandatory external argument labels; writing `sort(by:)` or `dropFirst(_:)` is a design decision about how the call reads. **Python** lets an author force the issue with keyword-only parameters - anything after a bare `*` in the signature cannot be passed positionally - and **C#** has named arguments available at the caller's discretion. All of these convert `render(true)` into `render(inline: true)`, which is a genuine improvement: a reader no longer has to open the callee to find out what the literal means. But note precisely what did not change. The caller still supplies a decision. The callee still branches. Adding a third mode still forces the boolean to become something else and still touches every call site. The coupling is unchanged; only its opacity was reduced. ## What options objects change - and break A common refactor in JavaScript is to replace positional flags with an options object: `render(node, { inline: true, strict: false })`. This buys naming and extensibility, and it changes the *kind* of coupling: the caller now depends on the shape of a record, which is stamp coupling. Stamp coupling is generally milder, but here it introduces a failure mode the boolean never had - a misspelled key, `{ inlnie: true }`, is silently absent, so the callee takes its default and the bug is invisible. TypeScript narrows this: excess-property checking rejects unknown keys when an object *literal* is passed directly to a typed parameter. Assign the same literal to a variable first and pass the variable, and the check does not apply, because the check is deliberately scoped to literals. That divergence between JavaScript and TypeScript, and within TypeScript between literal and variable, is worth being able to state exactly. ## What sum types change The structural fix is to stop passing a decision and start passing a *value from a closed vocabulary* that the language can check. In **Rust**, **Kotlin** (sealed classes and enums) and **Swift** (enums with associated values), a `match`/`when` over a closed type is checked for exhaustiveness. Two consequences follow. First, the parameter now names a domain concept - `Alignment::Center` rather than `true` - so the call site is self-explanatory without any labelling machinery. Second, when a third variant is added, the compiler enumerates every place that must be updated, so extending the vocabulary is a mechanical, complete change rather than a search. The caller still chooses, but it chooses from a published vocabulary rather than from knowledge of hidden branches - and that is the difference between depending on an interface and depending on an implementation. A further step removes the branch entirely: **Clojure's `defmulti`/`defmethod`** and **Common Lisp's CLOS generic functions** dispatch on a computed value or on argument types, so each mode is a separate method installed against the same generic operation. New modes are added by defining a new method without touching the original function. This is one direction of the expression problem - easy to add cases, harder to add operations - and it is the reason "replace the flag with polymorphism" is standard advice: dispatch is the language's own mechanism for choosing behaviour, and using it moves the choice out of hand-written control flow. ## When a boolean parameter is fine Not every boolean is control coupling. If the parameter is *data* the callee stores, compares or forwards - `setEnabled(true)`, `User(active: false)` - there is no branch being steered and nothing to fix. The distinction is whether the value selects an execution path or populates a value. ## The answer to give Name control coupling, state the caller-knows-the-branch test, then separate the three families: labels (Smalltalk, Swift, Python) improve legibility only; options objects (JavaScript, TypeScript) trade it for stamp coupling with a new silent-typo failure mode; closed sum types and multimethods (Rust, Kotlin, Swift, Clojure) make the vocabulary explicit and checkable, which is the only one of the three that changes the dependency rather than its presentation.

  • Is every boolean parameter control coupling?
    No. If the value is data the callee stores, compares or forwards - a flag being persisted on a record, for instance - nothing is being steered and there is no hidden branch for the caller to know about. Control coupling requires that the parameter's only role is choosing an execution path, which is why the reliable check is to look at the callee's body for a branch whose arms share almost nothing.
  • Named arguments make the call site readable. Why is that not a fix?
    Because readability and coupling are different properties. A label tells the reader what the value means, but the caller is still supplying a decision about the callee's internal control flow, and adding a third mode still changes the parameter's type and every call site. The dependency only changes when the choice becomes a value from a published, closed vocabulary that the language dispatches on or checks exhaustively.
  • What does replacing a flag with an options object cost in JavaScript?
    It converts control coupling into stamp coupling on a record shape, and it introduces silent failure: an unrecognised key is simply absent, so a typo produces default behaviour with no error at all. TypeScript's excess-property check rejects unknown keys in an object literal passed directly, but not when the same object is assigned to a variable first, so the safety net has a specific and worth-knowing hole.

saying these in an interview costs you the question

  • Claiming named or keyword arguments remove control coupling
  • Treating every boolean parameter as a defect, including ones that are plain data
  • Believing TypeScript rejects all unknown keys in an options object
  • Splitting into two methods and then having one immediately delegate to the other with the flag - the branch just moved
  • Saying "replace it with polymorphism" without being able to say what dispatch actually buys: a closed, checkable vocabulary and extension without editing the callee

context