skip to content

You need a logger, a transaction, and the entity available inside one function body. Compare nesting scope functions vs. using context parameters, and explain how to combine an extension receiver with context parameters.

level: middleimportance: should knowfreq 20%

answer

  1. nesting: works now, no flag, but clash-prone & deep
  2. context params: named, flat, experimental opt-in
  3. entity = extension receiver; services = context params
  4. callers still provide contexts (with(logger){ })
  5. data → normal param; ambient service → context

basics

~20 s

Nesting scope functions (with/apply) works today with no opt-in but gets verbose and clash-prone as you stack receivers. Context parameters declare the ambient dependencies once and reference them by name, but are experimental. You can keep the entity as the extension receiver and add the logger/transaction as context parameters.

solid answer

~40 s

Two approaches. Nesting: with(logger) { with(tx) { entity.apply { ... } } } — available now, no flags, but each level is an implicit receiver, so overlapping member names clash and the code nests deeply. Context parameters: context(logger: Logger, tx: Transaction) fun User.save() { logger.info(...); tx.commit(); this.persist() } — the entity stays the extension receiver (this), while logger and tx are named contexts referenced explicitly, so there's no shadowing and the function reads top-down. The trade-off is that context parameters are experimental (-Xcontext-parameters) and require callers to provide the contexts (e.g. with(logger) { with(tx) { user.save() } } or a caller that already has those contexts). Choose nesting for stable/public code and quick local scopes; choose context parameters for internal cross-cutting capabilities where you control opt-in.

code

kotlin · 11 lines
kotlin
interface Logger { fun info(m: String) }
interface Transaction { fun commit() }
class User(val name: String) { fun persist() = println("persisted $name") }

// experimental: -Xcontext-parameters
context(logger: Logger, tx: Transaction)
fun User.save() {
    logger.info("saving $name")  // context, by name
    persist()                    // this: User (extension receiver)
    tx.commit()                  // context, by name
}

go deeper

for a junior

Can nest with/apply to get objects in scope but may not know context parameters as an alternative.

for a middle

Compares both, maps entity→extension receiver and services→context params, and notes opt-in/clash trade-offs.

for a senior

Justifies the choice per situation (public vs internal) and reasons about context propagation and readability.

for a principal

Defines a team rule for the receiver/context split and where experimental contexts are permitted.

## The scenario Inside one function you want three things in scope: a `Logger` and a `Transaction` (ambient services) plus the `User` entity the function is about. ## Option A — nest scope functions (available today) ```kotlin fun saveUser(logger: Logger, tx: Transaction, user: User) = with(logger) { with(tx) { user.apply { info("saving $name") // Logger.info (outer receiver) persist() // User.persist (inner receiver) commit() // Transaction.commit (middle receiver) } } } ``` Pros: works with **no opt-in**, standard library only, fine for short local scopes. Cons: three implicit receivers stacked → if `Logger`, `Transaction`, `User` share a member name you get **shadowing/ambiguity**; deep nesting hurts readability; the *intent* (which object owns each call) is implicit. ## Option B — context parameters + extension receiver (experimental) Keep the **entity** as the **extension receiver** (it's what the function is *about*) and add the services as **context parameters**: ```kotlin // requires -Xcontext-parameters context(logger: Logger, tx: Transaction) fun User.save() { logger.info("saving $name") // named context — no clash persist() // this (User) — extension receiver tx.commit() // named context } ``` Call site provides the contexts: ```kotlin with(logger) { with(tx) { user.save() } } ``` Pros: flat body, **named** references so no shadowing, clear separation of *the object* (`this`) vs *capabilities* (`logger`, `tx`). Contexts also **propagate** so chained calls needn't re-provide them. Cons: **experimental** opt-in flag; not for stable public APIs; callers still must establish the contexts. ## The receiver/context split — a design rule - **Dispatch receiver**: the class the function lives on. - **Extension receiver** (`this`): the single thing the function is fundamentally about — here, `User`. - **Context parameters**: ambient capabilities/services it needs present — here, `Logger`, `Transaction`. This mapping keeps signatures intention-revealing: `User.save()` reads as "save this user", with logging/transaction as required ambient context. ## Choosing | Need | Prefer | |------|--------| | Public/stable API, no opt-in | Nesting or plain parameters | | Short local scope, one or two objects | Nesting (`with`/`apply`) | | Internal cross-cutting services, many call sites | Context parameters | | Avoid name clashes among scoped objects | Context parameters (named) | ## Gotchas - You can mix: an extension receiver **plus** context parameters on the same function. - Don't turn genuine **data** into a context — data belongs in normal parameters. - Even with context parameters, the **caller** must satisfy the contexts; that's often still a `with` at the boundary.

  • Why keep User as the extension receiver instead of also a context parameter?
    Because the function is fundamentally about the User — that's the idiomatic role of the extension receiver. Logger/Transaction are ambient capabilities, which is exactly what context parameters express.
  • Does the caller escape providing the logger/transaction with context parameters?
    No — the caller must have those contexts in scope (often via with(logger){ with(tx){ } } or a caller that itself declares them). Contexts then propagate down without re-providing at each nested call.

saying these in an interview costs you the question

  • Recommends context parameters for a stable public API without noting the opt-in cost
  • Makes the entity a context parameter and a service the extension receiver (inverted roles)
  • Claims nesting scope functions can't cause name clashes
  • Thinks context parameters remove the caller's obligation to provide contexts
  • Stuffs plain data into context parameters instead of normal parameters

context