Skip to content

Commit e9f2e46

Browse files
committed
feat: add Kotlin intermediate tour lambdas with receiver
1 parent 6d956ca commit e9f2e46

File tree

2 files changed

+308
-0
lines changed

2 files changed

+308
-0
lines changed

docs/kr.tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<toc-element hidden="true" topic="kotlin-tour-null-safety.md"/>
1414
<toc-element hidden="true" topic="kotlin-tour-intermediate-extension-functions.md"/>
1515
<toc-element hidden="true" topic="kotlin-tour-intermediate-scope-functions.md"/>
16+
<toc-element hidden="true" topic="kotlin-tour-intermediate-lambdas-receiver.md"/>
1617
</toc-element>
1718
<toc-element toc-title="Kotlin overview">
1819
<toc-element topic="multiplatform.topic"/>
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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

Comments
 (0)