Kotlin Help

Intermediate: Lambda expressions with receiver

In this chapter, you'll learn how to use receivers with another type of function, lambda expressions, and how they can help you create a domain-specific language.

Lambda expressions with receiver

In the beginner tour, you learned how to use lambda expressions. Lambda expressions can also have a receiver. In this case, lambda expressions can access any member functions or properties of the receiver without having to explicitly specify the receiver each time. Without these additional references, your code is easier to read and maintain.

The syntax for a lambda expression with receiver is different when you define the function type. First, write the receiver that you want to extend. Next, put a . and then complete the rest of your function type definition. For example:

MutableList<Int>.() -> Unit

This function type has:

  • MutableList<Int> as the receiver.

  • No function parameters within the parentheses ().

  • No return value: Unit.

Consider this example that draws shapes on a canvas:

class Canvas { fun drawCircle() = println("🟠 Drawing a circle") fun drawSquare() = println("🟥 Drawing a square") } // Lambda expression with receiver definition fun render(block: Canvas.() -> Unit): Canvas { val canvas = Canvas() // Use the lambda expression with receiver canvas.block() return canvas } fun main() { render { drawCircle() // 🟠 Drawing a circle drawSquare() // 🟥 Drawing a square } }

In this example:

  • The Canvas class has two functions that simulate drawing a circle or a square.

  • The render() function takes a block parameter and returns an instance of the Canvas class.

  • The block parameter is a lambda expression with receiver, where the Canvas class is the receiver.

  • The render() function creates an instance of the Canvas class and calls the block() lambda expression on the canvas instance, using it as the receiver.

  • The main() function calls the render() function with a lambda expression, which is passed to the block parameter.

  • Inside the lambda passed to the render() function, the program calls the drawCircle() and drawSquare() functions on an instance of the Canvas class.

    Because the drawCircle() and drawSquare() functions are called in the lambda expression with receiver, they can be called directly as if they are inside the Canvas class.

Lambda expressions with receiver are helpful when you want to create a domain-specific language (DSL). Since you have access to the receiver's member functions and properties without explicitly referencing the receiver, your code becomes leaner.

To demonstrate this, consider an example that configures items in a menu. Let's begin with a MenuItem class and a Menu class that contains a function to add items to the menu called item(), as well as a list of all menu items items:

class MenuItem(val name: String) class Menu(val name: String) { val items = mutableListOf<MenuItem>() fun item(name: String) { items.add(MenuItem(name)) } }

Let's use a lambda expression with receiver passed as a function parameter (init) to the menu() function that builds a menu as a starting point:

fun menu(name: String, init: Menu.() -> Unit): Menu { // Creates an instance of the Menu class val menu = Menu(name) // Calls the lambda expression with receiver init() on the class instance menu.init() return menu }

Now you can use the DSL to configure a menu and create a printMenu() function to print the menu structure to the console:

class MenuItem(val name: String) class Menu(val name: String) { val items = mutableListOf<MenuItem>() fun item(name: String) { items.add(MenuItem(name)) } } fun menu(name: String, init: Menu.() -> Unit): Menu { val menu = Menu(name) menu.init() return menu } //sampleStart fun printMenu(menu: Menu) { println("Menu: ${menu.name}") menu.items.forEach { println(" Item: ${it.name}") } } // Use the DSL fun main() { // Create the menu val mainMenu = menu("Main Menu") { // Add items to the menu item("Home") item("Settings") item("Exit") } // Print the menu printMenu(mainMenu) // Menu: Main Menu // Item: Home // Item: Settings // Item: Exit } //sampleEnd

As you can see, using a lambda expression with receiver greatly simplifies the code needed to create your menu. Lambda expressions are not only useful for setup and creation but also for configuration. They are commonly used in building DSLs for APIs, UI frameworks, and configuration builders to produce streamlined code, allowing you to focus more easily on the underlying code structure and logic.

Kotlin's ecosystem has many examples of this design pattern, such as in the buildList() and buildString() functions from the standard library.

Practice

Exercise 1

You have a fetchData() function that accepts a lambda expression with receiver. Update the lambda expression to use the append() function so that the output of your code is: Data received - Processed.

fun fetchData(callback: StringBuilder.() -> Unit) { val builder = StringBuilder("Data received") builder.callback() } fun main() { fetchData { // Write your code here // Data received - Processed } }
fun fetchData(callback: StringBuilder.() -> Unit) { val builder = StringBuilder("Data received") builder.callback() } fun main() { fetchData { append(" - Processed") println(this.toString()) // Data received - Processed } }

Exercise 2

You have a Button class and ButtonEvent and Position data classes. Write some code that triggers the onEvent() member function of the Button class to trigger a double-click event. Your code should print "Double click!".

class Button { fun onEvent(action: ButtonEvent.() -> Unit) { // Simulate a double-click event (not a right-click) val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) event.action() // Trigger the event callback } } data class ButtonEvent( val isRightClick: Boolean, val amount: Int, val position: Position ) data class Position( val x: Int, val y: Int ) fun main() { val button = Button() button.onEvent { // Write your code here // Double click! } }
class Button { fun onEvent(action: ButtonEvent.() -> Unit) { // Simulate a double-click event (not a right-click) val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) event.action() // Trigger the event callback } } data class ButtonEvent( val isRightClick: Boolean, val amount: Int, val position: Position ) data class Position( val x: Int, val y: Int ) fun main() { val button = Button() button.onEvent { if (!isRightClick && amount == 2) { println("Double click!") // Double click! } } }

Exercise 3

Write a function that creates a copy of a list of integers where every element is incremented by 1. Use the provided function skeleton that extends List<Int> with an incremented function.

fun List<Int>.incremented(): List<Int> { val originalList = this return buildList { // Write your code here } } fun main() { val originalList = listOf(1, 2, 3) val newList = originalList.incremented() println(newList) // [2, 3, 4] }
fun List<Int>.incremented(): List<Int> { val originalList = this return buildList { for (n in originalList) add(n + 1) } } fun main() { val originalList = listOf(1, 2, 3) val newList = originalList.incremented() println(newList) // [2, 3, 4] }

Next step

Intermediate: Classes and interfaces

03 September 2025