skip to content

Multiple Receivers & Context Parameters

Sometimes you need more than one implicit receiver, which today means nesting with and apply, and in newer Kotlin means context parameters. Interviewers use this to probe how current your knowledge of the language's direction is.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

How do you make two receivers available inside the same block using only standard scope functions, e.g. with(a) { b.apply { ... } }, and which receiver wins when both have a member with the same name?

level: juniorimportance: must knowfreq 55%

answer

  1. with(a){ b.apply{} } stacks two receivers
  2. innermost receiver wins on name clash
  3. this@with reaches the outer receiver
  4. apply/also return receiver; with/run/let return result
  5. use also/let to make one object 'it' and dodge clashes

basics

~20 s

Nest scope functions: with(a) { b.apply { ... } }. Inside the inner block both a and b are reachable. If both define the same name, the innermost receiver (b) is used unless you qualify it.

solid answer

~40 s

Scope functions like with(a) { } and b.apply { } install a as the receiver of the outer lambda and b as the receiver of the inner lambda. Nesting them stacks both receivers so the inner block can call members of both a (outer) and b (inner) without qualification. Resolution is innermost-first: when a name exists on both, the nearest receiver (b) shadows the outer one (a). To reach the shadowed outer member you qualify with a labeled this: this@with, or restructure. apply/also return the receiver; with/run/let return the lambda result. Use also/let when you prefer it as a named parameter (it) instead of an implicit receiver, which avoids ambiguity entirely. This stacking is exactly how type-safe builder DSLs nest scopes.

code

kotlin · 10 lines
kotlin
data class Server(val name: String) { fun log(m: String) = println("[$name] $m") }
class Request { var path: String = "/"; fun log(m: String) = println("req: $m") }

fun handle(server: Server, request: Request) = with(server) {  // this = server
    request.apply {                                            // this = request
        path = "/users"
        log("inner")        // Request.log (innermost)
        this@with.log("outer") // Server.log via label
    }
}

go deeper

for a junior

Can nest with/apply and knows it stacks both objects into scope.

for a middle

Explains innermost-first resolution and uses this@label to disambiguate; picks apply vs run by return type.

for a senior

Reaches for also/let to avoid clashes, reasons about readability of nested receivers, and knows when to flatten instead.

for a principal

Weighs nested-scope DSL ergonomics vs context parameters, and sets team conventions for receiver nesting depth.

## The problem A lambda with receiver runs its body as if `this` were some object. Standard library **scope functions** install such receivers. The common ones: - `with(a) { ... }` — `a` is `this` inside; returns the lambda result. - `a.run { ... }` — like `with` but an extension; `a` is `this`; returns lambda result. - `a.apply { ... }` — `a` is `this`; returns **`a`** (good for configuring then returning). - `a.also { ... }` — `a` is **`it`** (a parameter, not a receiver); returns `a`. - `a.let { ... }` — `a` is **`it`**; returns lambda result. ## Stacking two receivers To have **two** receivers active at once, nest receiver-style scope functions: ```kotlin with(server) { // this == server (outer receiver) request.apply { // this == request (inner receiver) // both reachable here: path = "/users" // request.path (inner) log("handling") // server.log(...) (outer, unqualified) } } ``` Inside the inner block the compiler searches receivers **innermost-first**. `path` resolves on `request`; `log` is not on `request`, so it falls through to `server`. ## Shadowing & qualification If both receivers expose the same member name, the **innermost wins** and shadows the outer. To reach the outer one, use a **labeled `this`**: ```kotlin with(outer) { inner.apply { name // inner.name (shadows) [email protected] // outer.name (explicitly qualified) } } ``` The label after `this@` is the function name (`this@with`, `this@apply`) or an explicit lambda label (`outer@ { }` → `this@outer`). ## Avoiding ambiguity Use `also`/`let` for one of the objects so it becomes `it` (a normal parameter), not an implicit receiver — then there is no name clash: ```kotlin with(server) { request.also { req -> req.path = "/users" log("handling " + req.path) } } ``` ## Picking the right function - Return the object configured → `apply`/`also`. - Return a computed result → `with`/`run`/`let`. - Want implicit `this` → `with`/`run`/`apply`. - Want explicit `it` → `let`/`also`. This nesting is the manual way to get multiple contexts; the language-level alternative is **context parameters** (`context(...)`), covered separately.

  • Why might you prefer also { it -> } over apply { } when nesting?
    also exposes the object as the named parameter it instead of an implicit receiver, so it can never clash with the outer receiver's members and the code reads more explicitly.
  • Does with(a){ b.run { } } stack receivers the same way as b.apply { }?
    Yes for receiver stacking — run also makes b the this. The difference is the return value: run returns the lambda result, apply returns b.

Nested rooms: standing in the inner room you can still reach shelves in the outer room, but if both rooms have a 'door' the nearest one is what 'the door' means.

saying these in an interview costs you the question

  • Thinks the outer receiver wins on a name clash
  • Doesn't know this@with / labeled this to reach a shadowed receiver
  • Confuses apply (returns receiver) with run/with (returns result)
  • Believes also/let provide an implicit this rather than it
  • Thinks you can only ever have one receiver in scope

context

open as a page

What problem do Kotlin's context parameters (context(...)) solve, how do they differ from an extension receiver, and how do you declare and call a function that requires a context?

level: middleimportance: should knowfreq 35%

basics

~20 s

Context parameters let a function require some surrounding object (like a logger or transaction) to be implicitly available, without it being a normal parameter or the receiver. You declare context(x: T) before the function and the caller must have a matching context in scope.

open as a page

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%

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.

open as a page

When multiple implicit receivers/contexts are in scope and several could resolve the same unqualified call, how does Kotlin decide, and how do you disambiguate?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Kotlin searches receivers from the innermost scope outward and uses the closest one that has a matching member; a nearer receiver shadows farther ones. If a single scope offers two equally valid candidates it's an ambiguity error. You disambiguate with labeled this (this@outer) or by qualifying the call.

open as a page

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%

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.

open as a page