Building DSLs (Lambdas with Receiver)
A lambda with receiver has the type T.() -> Unit , which makes this inside the block a T — the one feature that lets you build clean, readable domain-specific languages.
Learn Building DSLs (Lambdas with Receiver) in our free Kotlin course — a beginner-friendly interactive lesson with worked examples, a practice exercise and…
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You'll use apply and buildString as DSLs, build a tiny HTML builder, and learn what @DslMarker protects you from.
What You'll Learn in This Lesson
1️⃣ Lambdas With Receiver
A normal lambda parameter is (T) -> Unit . Add a receiver and it becomes T.() -> Unit : now inside the lambda, this is a T , so you call its members with no prefix. You invoke such a block with receiver.block() .
2️⃣ apply and buildString as DSLs
The standard library already ships receiver-based DSLs. apply runs a block with an object as this and returns it, perfect for configuration. buildString gives you a StringBuilder as this and returns the finished String .
3️⃣ A Tiny HTML Builder
Put it together: a builder class with methods that add children, plus a top-level entry function that runs a T.() -> Unit block. Each nested method that itself takes a receiver block creates a child builder, so the blocks nest and read like markup.
Real libraries like kotlinx.html and the Gradle Kotlin DSL are exactly this pattern, scaled up — and they use @DslMarker to keep nested receivers from clashing.
Your turn. Fill in the ___ blank, then run and compare.
Build a menu {' '} DSL that collects items and prints them joined.
📋 Quick Reference — DSL Building Blocks
Practice quiz
What is the type of a lambda with receiver?
- T.() -> R
- (T) -> R
- () -> T
- T -> R
Answer: T.() -> R. A lambda with receiver has type T.() -> R, where T is the receiver type.
Inside a lambda with receiver of type T, what does 'this' refer to?
- The enclosing function
- Always null
- An instance of T
- The first parameter named it
Answer: An instance of T. Inside a T.() -> Unit lambda, this is an instance of T, so members are called with no prefix.
How does T.() -> Unit differ from (T) -> Unit?
- They are identical in every way
- T.() -> Unit gives you 'this'; (T) -> Unit gives you a parameter like 'it'
- (T) -> Unit cannot be called
- T.() -> Unit takes no receiver
Answer: T.() -> Unit gives you 'this'; (T) -> Unit gives you a parameter like 'it'. The receiver form exposes the value as this; the parameter form exposes it as it.
What does the standard library's apply function return?
- The same object it was called on
- A new copy
- Unit
- A Boolean
Answer: The same object it was called on. apply runs the block with the object as receiver and returns that same object.
What does buildString give you as 'this' inside its block?
- A List
- A StringBuilder
- a Map
- an Int
Answer: A StringBuilder. buildString provides a StringBuilder as this and returns the finished String.
To run a receiver block on a receiver, you write...
- block(receiver)
- receiver -> block
- call(block)
- receiver.block()
Answer: receiver.block(). You invoke a receiver lambda with receiver.block().
What does @DslMarker prevent?
- Calling a method on an implicit outer receiver from a nested block
- Defining more than one builder class
- Using lambdas at all
- Returning a value from a DSL
Answer: Calling a method on an implicit outer receiver from a nested block. @DslMarker forbids accidental calls on an implicit outer receiver inside nested blocks.
Why do nested builder blocks read naturally like html { tag("p") { ... } }?
- Because of reflection
- Because each block's receiver is the appropriate builder, so inner calls need no prefix
- Because Kotlin shuffles the calls
- Because builders are global functions
Answer: Because each block's receiver is the appropriate builder, so inner calls need no prefix. Each level's receiver is the relevant builder, so its members are callable without a prefix.
What does a top-level DSL entry function typically do?
- Creates a root builder, runs a T.() -> Unit block on it, and returns the result
- Only prints text
- Throws an exception
- Returns Unit and ignores the block
Answer: Creates a root builder, runs a T.() -> Unit block on it, and returns the result. It builds a root, invokes the receiver block, and returns the built object.
Which standard-library function is the simplest example of a builder-style receiver DSL?
- println
- apply
- listOf
- require
Answer: apply. apply is the simplest receiver-based builder: configure inline and get the object back.