skip to content

Kotlin had an experimental 'context receivers' prototype that was redesigned into 'context parameters'. What changed, why, and what does that mean for code and library design today?

level: principalimportance: nice to knowfreq 12%

answer

  1. receivers prototype: context(Type) unnamed, members as implicit this
  2. parameters design: context(name: Type) named, referenced explicitly
  3. named form kills cross-context member clashes
  4. -Xcontext-receivers deprecated/removed; -Xcontext-parameters in 2.2
  5. keep experimental contexts out of stable public APIs

basics

~20 s

Old context receivers (context(Logger)) made the context an unnamed implicit this, which caused clashes and confusion. They were redesigned into context parameters (context(logger: Logger)) where each context is a named value. The named version is the supported direction; the old prototype is being removed.

solid answer

~40 s

The first prototype, context receivers (flag -Xcontext-receivers), declared unnamed contexts: context(Logger, Transaction) fun f(). Each context became an additional implicit receiver — members were callable unqualified as if this — which produced ambiguity when several contexts shared member names, made it impossible to name a context you wanted to reference, and blurred the line between receivers and dependencies. The redesign, context parameters (flag -Xcontext-parameters, Kotlin 2.2), gives every context a name: context(logger: Logger, tx: Transaction). You reference each by name (logger.info(...)), and a context parameter is no longer an implicit dot-receiver. This removes the ambiguity, supports anonymous-context values via _, and aligns with capability-passing designs. Both are experimental; the receivers prototype is deprecated/being removed, so new code should use named context parameters and treat the feature as not yet stable for public APIs.

code

kotlin · 10 lines
kotlin
// OLD (context receivers, being removed):
// context(Logger, Transaction)
// fun saveUser(u: User) { info("saving"); commit() }

// NEW (context parameters, Kotlin 2.2, -Xcontext-parameters):
context(logger: Logger, tx: Transaction)
fun saveUser(u: User) {
    logger.info("saving $u")   // named reference, no clash
    tx.commit()
}

go deeper

for a junior

Likely unaware of the distinction; may only know one form exists.

for a middle

Knows named context parameters are the current syntax but may not know the history or removal of the prototype.

for a senior

Explains why naming fixed the ambiguity and keeps the feature out of stable APIs.

for a principal

Sets org policy on adopting an evolving experimental feature, plans migration, and designs capability-passing APIs with the right receiver/context split.

## Two iterations of the same idea Kotlin explored letting a function require **more than one implicit context** beyond its single dispatch + single extension receiver. It shipped in two experimental forms. ### 1. Context receivers (older prototype) Flag: `-Xcontext-receivers`. Contexts were **unnamed**, declared by type only: ```kotlin // OLD prototype context(Logger, Transaction) fun saveUser(u: User) { info("saving") // Logger.info — context member as implicit this commit() // Transaction.commit — also implicit } ``` Each context behaved like an **extra implicit receiver**: its members were callable unqualified, as though `this`. Problems: - **Name clashes**: if `Logger` and `Transaction` both had a member with the same name, calls were ambiguous. - **No name to reference**: you couldn't say "this particular logger" — there was no identifier, only `this@Logger`-style qualification. - **Conceptual blur**: contexts looked like receivers, encouraging member-style calls and confusing the model. ### 2. Context parameters (current design) Flag: `-Xcontext-parameters` (Kotlin **2.2**). Every context gets a **name**: ```kotlin // CURRENT design context(logger: Logger, tx: Transaction) fun saveUser(u: User) { logger.info("saving") // referenced by name tx.commit() // referenced by name } ``` Key differences: - Contexts are **named values**, referenced explicitly (`logger`, `tx`) — no implicit-`this` member calls, so **no clash** between two contexts. - A context parameter is **not** a dot-receiver: you don't call `someLogger.saveUser()`. - You can use `_` for a context you require but don't name. - The model is now "implicit, compiler-checked parameters" rather than "extra receivers". ## Why the change - Eliminates the ambiguity from overlapping context member names. - Makes intent explicit and code readable (named references vs. mystery `this`). - Cleaner foundation for **capability/effect-passing** patterns (e.g., passing a `Raise<E>`-style capability) and future tooling. ## Implications for design today - **Use named context parameters** in any new experimentation; the receivers prototype is **deprecated and being removed** — don't start there. - **Still experimental**: requires the opt-in flag and may change. Keep it out of **stable public library APIs**; the moment it appears in a signature, consumers must opt in too. - **Migration**: convert `context(Type)` to `context(name: Type)` and switch unqualified context-member calls to `name.member()`. - **Library boundaries**: prefer ordinary parameters or interfaces for public surfaces; reserve contexts for internal cross-cutting concerns until stabilized. - **Don't over-apply**: contexts are for *ambient* dependencies (logging, transactions, scopes). Data should stay a normal parameter; the object an API is fundamentally *about* should stay the extension receiver. ## Quick mental model Extension receiver = the thing the function is *about*. Context parameter = a *capability/service* the function needs present. Dispatch receiver = the class it lives on. Contexts let you require several of the second kind without faking them as receivers.

  • Why was naming each context the central fix?
    Unnamed contexts were implicit receivers, so overlapping member names were ambiguous and you couldn't refer to a specific context. Names make every context an explicit, non-receiver reference, removing both problems.
  • Would you put a context parameter in a public, stable library API today?
    No. It's experimental and opt-in; exposing it forces consumers to enable the flag and risks breakage when the feature evolves. Use plain parameters or interfaces on public surfaces and reserve contexts for internal code.

saying these in an interview costs you the question

  • Thinks context receivers and context parameters are identical syntax
  • Recommends the removed -Xcontext-receivers prototype for new code
  • Treats the feature as stable and fine for public API signatures
  • Claims context parameters are accessed as implicit this like the old receivers
  • Uses contexts for ordinary data parameters rather than ambient capabilities

context