|
| 1 | +[//]: # (title: Intermediate: Lambda expressions with receiver) |
| 2 | + |
| 3 | +<tldr> |
| 4 | + <p><img src="icon-1-done.svg" width="20" alt="First step" /> <a href="kotlin-tour-intermediate-extension-functions.md">Extension functions</a><br /> |
| 5 | + <img src="icon-2-done.svg" width="20" alt="Second step" /> <a href="kotlin-tour-intermediate-scope-functions.md">Scope functions</a><br /> |
| 6 | + <img src="icon-3.svg" width="20" alt="Third step" /> <strong>Lambda expressions with receiver</strong><br /> |
| 7 | + <img src="icon-4-todo.svg" width="20" alt="Fourth step" /> <a href="kotlin-tour-intermediate-classes-interfaces.md">Classes and interfaces</a><br /> |
| 8 | + <img src="icon-5-todo.svg" width="20" alt="Fifth step" /> <a href="kotlin-tour-intermediate-objects.md">Objects</a><br /> |
| 9 | + <img src="icon-6-todo.svg" width="20" alt="Sixth step" /> <a href="kotlin-tour-intermediate-open-special-classes.md">Open and special classes</a><br /> |
| 10 | + <img src="icon-7-todo.svg" width="20" alt="Seventh step" /> <a href="kotlin-tour-intermediate-properties.md">Properties</a><br /> |
| 11 | + <img src="icon-8-todo.svg" width="20" alt="Eighth step" /> <a href="kotlin-tour-intermediate-null-safety.md">Null safety</a><br /> |
| 12 | + <img src="icon-9-todo.svg" width="20" alt="Ninth step" /> <a href="kotlin-tour-intermediate-libraries-and-apis.md">Libraries and APIs</a></p> |
| 13 | +</tldr> |
| 14 | + |
| 15 | +In this chapter, you'll learn how to use receiver objects with another type of function, lambda expressions, and how they |
| 16 | +can help you create a domain-specific language. |
| 17 | + |
| 18 | +## Lambda expressions with receiver |
| 19 | + |
| 20 | +In the beginner tour, you learned how to use [lambda expressions](kotlin-tour-functions.md#lambda-expressions). Lambda expressions can also have a receiver. |
| 21 | +In this case, lambda expressions can access any member functions or properties of the receiver object without having |
| 22 | +to explicitly specify the receiver object each time. Without these additional references, your code is easier to read and maintain. |
| 23 | + |
| 24 | +> Lambda expressions with receiver are also known as function literals with receiver. |
| 25 | +> |
| 26 | +{style="tip"} |
| 27 | + |
| 28 | +The syntax for a lambda expression with receiver is different when you define the function type. First, write the receiver |
| 29 | +object that you want to extend. Next, put a `.` and then complete the rest of your function type definition. For example: |
| 30 | + |
| 31 | +```kotlin |
| 32 | +MutableList<Int>.() -> Unit |
| 33 | +``` |
| 34 | + |
| 35 | +This function type has: |
| 36 | + |
| 37 | +* `MutableList<Int>` as the receiver type. |
| 38 | +* No function parameters within the parentheses `()`. |
| 39 | +* No return value: `Unit`. |
| 40 | + |
| 41 | +Consider this example that extends the `StringBuilder` class: |
| 42 | + |
| 43 | +```kotlin |
| 44 | +fun main() { |
| 45 | + // Lambda expression with receiver definition |
| 46 | + fun StringBuilder.appendText() { append("Hello!") } |
| 47 | + |
| 48 | + // Use the lambda expression with receiver |
| 49 | + val stringBuilder = StringBuilder() |
| 50 | + stringBuilder.appendText() |
| 51 | + println(stringBuilder.toString()) |
| 52 | + // Hello! |
| 53 | +} |
| 54 | +``` |
| 55 | +{kotlin-runnable="true" kotlin-min-compiler-version="1.3" id="kotlin-intermediate-tour-lambda-expression-with-receiver"} |
| 56 | + |
| 57 | +In this example: |
| 58 | + |
| 59 | +* The `StringBuilder` class is the receiver type. |
| 60 | +* The function type of the lambda expression has no function parameters `()` and has no return value `Unit`. |
| 61 | +* The lambda expression calls the `append()` member function from the `StringBuilder` class and uses the string `"Hello!"` as the function parameter. |
| 62 | +* An instance of the `StringBuilder` class is created. |
| 63 | +* The lambda expression assigned to `appendText` is called on the `stringBuilder` instance. |
| 64 | +* The `stringBuilder` instance is converted to string with the `toString()` function and printed via the `println()` function. |
| 65 | + |
| 66 | +Lambda expressions with receiver are helpful when you want to create a domain-specific language (DSL). Since you have |
| 67 | +access to the receiver object's member functions and properties without explicitly referencing the receiver, your code |
| 68 | +becomes leaner. |
| 69 | + |
| 70 | +To demonstrate this, consider an example that configures items in a menu. Let's begin with a `MenuItem` class and a |
| 71 | +`Menu` class that contains a function to add items to the menu called `item()`, as well as a list of all menu items `items`: |
| 72 | + |
| 73 | +```kotlin |
| 74 | +class MenuItem(val name: String) |
| 75 | + |
| 76 | +class Menu(val name: String) { |
| 77 | + val items = mutableListOf<MenuItem>() |
| 78 | + |
| 79 | + fun item(name: String) { |
| 80 | + items.add(MenuItem(name)) |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Let's use a lambda expression with receiver passed as a function parameter (`init`) to the `menu()` function that builds |
| 86 | +a menu as a starting point. You'll notice that the code follows a similar approach to the previous example with the |
| 87 | +`StringBuilder` class: |
| 88 | + |
| 89 | +```kotlin |
| 90 | +fun menu(name: String, init: Menu.() -> Unit): Menu { |
| 91 | + // Creates an instance of the Menu class |
| 92 | + val menu = Menu(name) |
| 93 | + // Calls the lambda expression with receiver init() on the class instance |
| 94 | + menu.init() |
| 95 | + return menu |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +Now you can use the DSL to configure a menu and create a `printMenu()` function to print the menu structure to the console: |
| 100 | + |
| 101 | +```kotlin |
| 102 | +class MenuItem(val name: String) |
| 103 | + |
| 104 | +class Menu(val name: String) { |
| 105 | + val items = mutableListOf<MenuItem>() |
| 106 | + |
| 107 | + fun item(name: String) { |
| 108 | + items.add(MenuItem(name)) |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +fun menu(name: String, init: Menu.() -> Unit): Menu { |
| 113 | + val menu = Menu(name) |
| 114 | + menu.init() |
| 115 | + return menu |
| 116 | +} |
| 117 | + |
| 118 | +//sampleStart |
| 119 | +fun printMenu(menu: Menu) { |
| 120 | + println("Menu: ${menu.name}") |
| 121 | + menu.items.forEach { println(" Item: ${it.name}") } |
| 122 | +} |
| 123 | + |
| 124 | +// Use the DSL |
| 125 | +fun main() { |
| 126 | + // Create the menu |
| 127 | + val mainMenu = menu("Main Menu") { |
| 128 | + // Add items to the menu |
| 129 | + item("Home") |
| 130 | + item("Settings") |
| 131 | + item("Exit") |
| 132 | + } |
| 133 | + |
| 134 | + // Print the menu |
| 135 | + printMenu(mainMenu) |
| 136 | + // Menu: Main Menu |
| 137 | + // Item: Home |
| 138 | + // Item: Settings |
| 139 | + // Item: Exit |
| 140 | +} |
| 141 | +//sampleEnd |
| 142 | +``` |
| 143 | +{kotlin-runnable="true" kotlin-min-compiler-version="1.3" id="kotlin-intermediate-tour-lambda-expression-with-receiver-dsl"} |
| 144 | + |
| 145 | +As you can see, using a lambda expression with receiver greatly simplifies the code needed to create your menu. Lambda |
| 146 | +expressions are not only useful for setup and creation but also for configuration. They are commonly used in building |
| 147 | +DSLs for APIs, UI frameworks, and configuration builders to produce streamlined code, allowing you to focus more easily |
| 148 | +on the underlying code structure and logic. |
| 149 | + |
| 150 | +Kotlin's ecosystem has many examples of this design pattern, such as in the [`buildList()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/build-list.html) |
| 151 | +and [`buildString()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/build-string.html) functions from the |
| 152 | +standard library. |
| 153 | + |
| 154 | +> Lambda expressions with receivers can be combined with **type-safe builders** in Kotlin to make DSLs that detect any problems |
| 155 | +> with types at compile time rather than at runtime. To learn more, see [Type-safe builders](type-safe-builders.md). |
| 156 | +> |
| 157 | +{style="tip"} |
| 158 | + |
| 159 | +## Practice |
| 160 | + |
| 161 | +### Exercise 1 {initial-collapse-state="collapsed" collapsible="true" id="lambda-receivers-exercise-1"} |
| 162 | + |
| 163 | +You have a `fetchData()` function that accepts a lambda expression with receiver. Update the lambda expression to use |
| 164 | +the `append()` function so that the output of your code is: `Data received - Processed`. |
| 165 | + |
| 166 | +|---|---| |
| 167 | +```kotlin |
| 168 | +fun fetchData(callback: StringBuilder.() -> Unit) { |
| 169 | + val builder = StringBuilder("Data received") |
| 170 | + builder.callback() |
| 171 | +} |
| 172 | + |
| 173 | +fun main() { |
| 174 | + fetchData { |
| 175 | + // Write your code here |
| 176 | + } |
| 177 | +} |
| 178 | +``` |
| 179 | +{validate="false" kotlin-runnable="true" kotlin-min-compiler-version="1.3" id="kotlin-tour-lambda-receivers-exercise-1"} |
| 180 | + |
| 181 | +|---|---| |
| 182 | +```kotlin |
| 183 | +fun fetchData(callback: StringBuilder.() -> Unit) { |
| 184 | + val builder = StringBuilder("Data received") |
| 185 | + builder.callback() |
| 186 | +} |
| 187 | + |
| 188 | +fun main() { |
| 189 | + fetchData { |
| 190 | + append(" - Processed") |
| 191 | + println(this.toString()) |
| 192 | + } |
| 193 | +} |
| 194 | +``` |
| 195 | +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="Example solution" id="kotlin-tour-lambda-receivers-solution-1"} |
| 196 | + |
| 197 | +### Exercise 2 {initial-collapse-state="collapsed" collapsible="true" id="lambda-receivers-exercise-2"} |
| 198 | + |
| 199 | +You have a `Button` class and `ButtonEvent` and `Position` data classes. Write some code that triggers the `onEvent()` |
| 200 | +member function of the `Button` class to trigger a double-click event. Your code should print `"Double click!"`. |
| 201 | + |
| 202 | +```kotlin |
| 203 | +class Button { |
| 204 | + fun onEvent(action: ButtonEvent.() -> Unit) { |
| 205 | + // Simulate a double-click event (not a right-click) |
| 206 | + val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) |
| 207 | + event.action() // Trigger the event callback |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +data class ButtonEvent( |
| 212 | + val isRightClick: Boolean, |
| 213 | + val amount: Int, |
| 214 | + val position: Position |
| 215 | +) |
| 216 | + |
| 217 | +data class Position( |
| 218 | + val x: Int, |
| 219 | + val y: Int |
| 220 | +) |
| 221 | + |
| 222 | +fun main() { |
| 223 | + val button = Button() |
| 224 | + |
| 225 | + button.onEvent { |
| 226 | + // Write your code here |
| 227 | + } |
| 228 | +} |
| 229 | +``` |
| 230 | +{validate="false" kotlin-runnable="true" kotlin-min-compiler-version="1.3" id="kotlin-tour-lambda-receivers-exercise-2"} |
| 231 | + |
| 232 | +|---|---| |
| 233 | +```kotlin |
| 234 | +class Button { |
| 235 | + fun onEvent(action: ButtonEvent.() -> Unit) { |
| 236 | + // Simulate a double-click event (not a right-click) |
| 237 | + val event = ButtonEvent(isRightClick = false, amount = 2, position = Position(100, 200)) |
| 238 | + event.action() // Trigger the event callback |
| 239 | + } |
| 240 | +} |
| 241 | + |
| 242 | +data class ButtonEvent( |
| 243 | + val isRightClick: Boolean, |
| 244 | + val amount: Int, |
| 245 | + val position: Position |
| 246 | +) |
| 247 | + |
| 248 | +data class Position( |
| 249 | + val x: Int, |
| 250 | + val y: Int |
| 251 | +) |
| 252 | + |
| 253 | +fun main() { |
| 254 | + val button = Button() |
| 255 | + |
| 256 | + button.onEvent { |
| 257 | + if (!isRightClick && amount == 2) { |
| 258 | + println("Double click!") |
| 259 | + } |
| 260 | + } |
| 261 | +} |
| 262 | +``` |
| 263 | +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="Example solution" id="kotlin-tour-lambda-receivers-solution-2"} |
| 264 | + |
| 265 | +### Exercise 3 {initial-collapse-state="collapsed" collapsible="true" id="lambda-receivers-exercise-3"} |
| 266 | + |
| 267 | +Write a function that creates a copy of a list of integers where every element is incremented by 1. Use the provided |
| 268 | +function skeleton that extends `List<Int>` with an `incremented` function. |
| 269 | + |
| 270 | +```kotlin |
| 271 | +fun List<Int>.incremented(): List<Int> { |
| 272 | + val originalList = this |
| 273 | + return buildList { |
| 274 | + // Write your code here |
| 275 | + } |
| 276 | +} |
| 277 | + |
| 278 | +fun main() { |
| 279 | + val originalList = listOf(1, 2, 3) |
| 280 | + val newList = originalList.incremented() |
| 281 | + println(newList) |
| 282 | + // [2, 3, 4] |
| 283 | +} |
| 284 | +``` |
| 285 | +{validate="false" kotlin-runnable="true" kotlin-min-compiler-version="1.3" id="kotlin-tour-lambda-receivers-exercise-3"} |
| 286 | + |
| 287 | +|---|---| |
| 288 | +```kotlin |
| 289 | +fun List<Int>.incremented(): List<Int> { |
| 290 | + val originalList = this |
| 291 | + return buildList { |
| 292 | + for (n in originalList) add(n + 1) |
| 293 | + } |
| 294 | +} |
| 295 | + |
| 296 | +fun main() { |
| 297 | + val originalList = listOf(1, 2, 3) |
| 298 | + val newList = originalList.incremented() |
| 299 | + println(newList) |
| 300 | + // [2, 3, 4] |
| 301 | +} |
| 302 | +``` |
| 303 | +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="Example solution" id="kotlin-tour-lambda-receivers-solution-3"} |
| 304 | + |
| 305 | +## Next step |
| 306 | + |
| 307 | +[Intermediate: Classes and interfaces](kotlin-tour-intermediate-classes-interfaces.md) |
0 commit comments