skip to content

Implicit Receiver Resolution

Nested receiver lambdas stack, and an unqualified call binds to the innermost receiver that has such a member, with this@Outer available to disambiguate. This resolution order is what makes accidental cross-scope calls possible in the first place.

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

questions

5

In a type-safe builder like an HTML DSL, what is an "implicit receiver" and why can you call functions like body { ... } inside html { ... } without writing any object name before them?

level: juniorimportance: must knowfreq 60%

answer

  1. A.() -> Unit = lambda with receiver
  2. Implicit receiver = the this you don't type
  3. Unqualified call binds to receiver's members
  4. Same idea as an extension function, but as a value
  5. Fully static — no reflection

basics

~20 s

A lambda with a receiver runs as if its body were inside a special object. That object is the 'implicit receiver', so its members can be called by name alone, with no dot before them.

solid answer

~40 s

A function whose last parameter has a receiver type like Html.() -> Unit invokes its lambda with an Html instance as the implicit receiver. Inside that lambda, this refers to the Html object, so calling body { } resolves to this.body { }. You don't name the receiver because the compiler binds unqualified members to it automatically. This is exactly how builder functions such as html { } expose body, head, and other members directly. The receiver type is part of the function type (A.() -> Unit), so the compiler knows which members are in scope before runtime. Builders nest these receiver lambdas so each level adds the right object as the current this.

code

kotlin · 10 lines
kotlin
fun html(init: Html.() -> Unit): Html = Html().apply(init)

class Html { fun body(init: Body.() -> Unit) = Body().init() }
class Body { fun p(text: String) = println(text) }

html {        // this: Html
    body {    // this.body { } -> this: Body
        p("hello")  // this.p("hello")
    }
}

go deeper

for a junior

Can state that the lambda runs against an object and that object's members are callable without a name.

for a middle

Names the A.() -> Unit type and explains the builder function passes an instance as receiver.

for a senior

Connects it to extension-function mechanics and emphasizes it is fully static / compile-time.

for a principal

Frames it as the enabling primitive for type-safe builder DSLs and contrasts with parameter-style lambdas for API ergonomics.

## What an implicit receiver is Kotlin has a function type called a **lambda with receiver**, written `A.() -> Unit`. When you call a function that takes such a lambda, you supply an instance of `A`, and inside the lambda body that instance becomes the **implicit receiver** — accessible as `this` and, crucially, usable *without* writing `this.` before each member. Think of an ordinary extension function: `fun Html.body() { ... }` — inside it, `this` is the `Html`. A lambda with receiver is the same idea, but for a lambda value. ## Why body { } works with no object name Given a builder: ```kotlin class Html { fun body(init: Body.() -> Unit) { /* ... */ } } class Body { fun p(text: String) { /* ... */ } } fun html(init: Html.() -> Unit): Html { val h = Html() h.init() // calls the lambda WITH h as receiver return h } ``` Usage: ```kotlin html { // 'this' here is the Html body { // resolves to this.body { } (Html.body) p("hi") // 'this' here is the Body; resolves to this.p("hi") } } ``` The compiler sees the receiver **type** baked into the function signature (`Html.() -> Unit`), so it knows `body` is a member of the implicit receiver and lets you call it unqualified. There is no runtime reflection — it is fully static. ## Key vocabulary - **Receiver**: the object a function/lambda runs against (the thing `this` points to). - **Implicit receiver**: a receiver you don't have to spell out; the compiler supplies it. - **Lambda with receiver**: a lambda whose type carries that receiver, `A.() -> Unit`. This mechanism is the foundation of every type-safe builder DSL (HTML builders, Gradle Kotlin DSL, `buildString`, `apply`).

  • How is a lambda with receiver different from a normal lambda that takes a parameter?
    A normal lambda `(A) -> Unit` gives you `it`/a named param you must reference explicitly; a receiver lambda `A.() -> Unit` makes that argument the implicit `this`, so its members are callable unqualified.
  • Is the receiver resolved at compile time or runtime?
    At compile time. The receiver type is part of the function type, so the compiler statically knows which members are in scope; there's no runtime lookup.

Like being a guest in someone's house: you can grab a cup from 'the kitchen' without saying whose kitchen — context makes it clear.

saying these in an interview costs you the question

  • Saying the receiver is found by runtime reflection
  • Confusing A.() -> Unit with (A) -> Unit (passing it vs being this)
  • Claiming you must always write this.body explicitly
  • Thinking implicit receivers are a special HTML-only feature rather than a language mechanism

context

open as a page

When receiver lambdas are nested, several implicit receivers are in scope at once. How does Kotlin decide which receiver an unqualified call resolves against?

level: middleimportance: must knowfreq 55%

basics

~10 s

The closest (innermost) receiver wins. Kotlin looks at the nearest enclosing receiver first; if it has a matching member, that's used. Only if it doesn't does it look further out.

open as a page

What does this@Html mean inside a nested builder, and when do you need qualified this (this@Label) instead of a bare this or an unqualified call?

level: middleimportance: should knowfreq 45%

basics

~10 s

this@Html means 'the Html receiver specifically', not whichever receiver is innermost. You use it when an outer receiver is shadowed by an inner one and you need to reach the outer object explicitly.

open as a page

A teammate's HTML-style DSL compiles, but a tag added inside an inner block mysteriously attaches to the outer element. Diagnose how implicit receiver resolution causes this, and give two concrete fixes.

level: seniorimportance: should knowfreq 30%

basics

~20 s

The inner block lacks that builder method, so the call silently falls through to the outer receiver, which has it. Fix by adding the method to the inner type, or annotate the DSL with @DslMarker so such cross-level calls become compile errors.

open as a page

Given multiple implicit receivers plus extension functions in scope, walk through Kotlin's priority order for resolving an unqualified call. Where do member functions of inner vs outer receivers and imported extensions sit?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Kotlin tries the closest receiver first, considering both its members and extensions on it, before moving to outer receivers. A nearer receiver — member or applicable extension — beats anything reachable only through a farther receiver.

open as a page