Lambdas Workbook (Part 5): inline and Receivers
Eleven short exercises on inline functions and lambdas with a receiver — marking a function inline, reified, receiver lambda types, apply and buildString, and writing a receiver-based builder.
Practice problems for Why Lambdas Are Free, and Lambdas That Read Like Syntax. Each takes a minute or two. Every exercise has a runnable editor — fill in the TODO, press Run (the code compiles on JetBrains’ Kotlin server), then click Show answer to check yourself. Nothing here is a trick question, just direct practice of the syntax from the lesson.
inline
1. Mark it inline
Make this higher-order function’s lambda cost nothing at runtime:
fun runTwice(block: () -> Unit) { block(); block() }
fun runTwice(block: () -> Unit) { block(); block() }
fun main() {
// TODO: mark runTwice inline above — behavior is identical, but no lambda object is created
runTwice { println("hi") }
} Show answer Hide answer
inline fun runTwice(block: () -> Unit) { block(); block() } 2. What inlining produces
Given inline fun repeatTimes(n: Int, action: (Int) -> Unit) { for (i in 0 until n) action(i) }, write the code that repeatTimes(2) { println(it) } effectively becomes after inlining.
fun main() {
// TODO: write the code that repeatTimes(2) { println(it) } becomes after inlining
} Show answer Hide answer
for (i in 0 until 2) println(i)No lambda object, no extra call — the body is copied in.
3. reified
Write inline fun <reified T> Any.asOrNull(): T? returning the receiver as a T, or null on a mismatch.
inline fun <reified T> Any.asOrNull(): T? = null // TODO: return this as a T, or null on mismatch
fun main() {
val x: Any = "hello"
println(x.asOrNull<String>())
println(x.asOrNull<Int>())
} Show answer Hide answer
inline fun <reified T> Any.asOrNull(): T? = this as? Treified keeps T at runtime; it works only because the function is inline.
4. noinline
In inline fun run2(a: () -> Unit, b: () -> Unit), mark b so it is not inlined (so you can store it in a variable).
inline fun run2(a: () -> Unit, b: () -> Unit) {
a()
b()
// TODO: to store b in a variable (val saved = b), mark b as noinline in the signature
}
fun main() {
run2({ println("a") }, { println("b") })
} Show answer Hide answer
inline fun run2(a: () -> Unit, noinline b: () -> Unit) {
a()
val saved = b
saved()
} lambdas with a receiver
5. A receiver lambda type
Declare val build of type StringBuilder.() -> Unit whose body appends "hi".
fun main() {
val build: StringBuilder.() -> Unit = { } // TODO: append "hi"
val sb = StringBuilder()
sb.build()
println(sb.toString())
} Show answer Hide answer
val build: StringBuilder.() -> Unit = { append("hi") }The StringBuilder. prefix makes this inside the lambda a StringBuilder, so append needs no qualifier.
6. Configure with apply
Use apply to create a StringBuilder, append "a" then "b", and keep the builder.
fun main() {
val sb = StringBuilder() // TODO: use apply to append "a" then "b"
println(sb.toString())
} Show answer Hide answer
val sb = StringBuilder().apply {
append("a")
append("b")
} 7. buildString
Use buildString to assemble "Hello, world".
fun main() {
val text = "" // TODO: use buildString to assemble "Hello, " + "world"
println(text)
} Show answer Hide answer
val text = buildString {
append("Hello, ")
append("world")
} 8. apply returns the object
Configure a File("out.txt") by calling createNewFile() via apply, assigning the resulting File to f.
import java.io.File
fun main() {
val f = File("out.txt") // TODO: use apply to call createNewFile() and keep the File
println(f.name)
} Show answer Hide answer
val f = File("out.txt").apply { createNewFile() }apply returns the receiver, so the whole expression is the configured File.
your own receiver builder
9. A receiver-lambda function
Write buildInts(block: MutableList<Int>.() -> Unit): List<Int> that creates a list, applies the block to it, and returns it.
fun main() {
fun buildInts(block: MutableList<Int>.() -> Unit): List<Int> {
// TODO: create a list, apply block to it, then return it
return emptyList()
}
val result = buildInts { }
println(result)
} Show answer Hide answer
fun buildInts(block: MutableList<Int>.() -> Unit): List<Int> {
val list = mutableListOf<Int>()
list.block()
return list
} 10. Use your builder
Using buildInts, build the list [1, 2, 3] — calling add without any prefix.
fun main() {
fun buildInts(block: MutableList<Int>.() -> Unit): List<Int> {
val list = mutableListOf<Int>()
list.block()
return list
}
val result = buildInts {
// TODO: add 1, 2, 3 with no prefix
}
println(result)
} Show answer Hide answer
buildInts {
add(1)
add(2)
add(3)
}Inside the block the receiver is the MutableList, so add resolves to this.add.
11. Why the bare calls work
In buildInts { add(10) }, what is add actually being called on, and why don’t you write it explicitly?
fun main() {
fun buildInts(block: MutableList<Int>.() -> Unit): List<Int> {
val list = mutableListOf<Int>()
list.block()
return list
}
val result = buildInts {
// TODO: add 10 — try it both ways, this.add(10) and add(10)
}
println(result)
} Show answer Hide answer
buildInts {
this.add(10) // 'this' is the MutableList — the receiver
add(10) // identical: the receiver is implicit
}A receiver lambda makes the receiver this, and this. is optional — exactly like inside a member function.
Going deeper: reified types and receiver lambdas
12. A type check that survives erasure
Implement firstOfType so firstOfType<String>() on a mixed list returns the first String. It needs the concrete type at runtime — which only an inline function with a reified parameter can provide.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun firstOfType() {
val mixed: List<Any> = listOf(1, "two", 3.0, "four")
Assert.assertEquals("two", mixed.firstOfType<String>())
Assert.assertEquals(3.0, mixed.firstOfType<Double>())
}
}
//sampleStart
inline fun <reified T> List<*>.firstOfType(): T? =
null // TODO: first element that is a T, or null
//sampleEnd Show answer Hide answer
inline fun <reified T> List<*>.firstOfType(): T? =
firstOrNull { it is T } as T?it is T compiles only because reified keeps T available at each call site — impossible without inline. This is exactly how filterIsInstance<T>() works.
13. A lambda with a receiver
buildString takes a StringBuilder.() -> Unit — a lambda whose receiver is a StringBuilder, so you call its methods with no prefix. Use it to produce "Hello, world".
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun greeting() {
Assert.assertEquals("Hello, world", greeting())
}
}
//sampleStart
fun greeting(): String =
"" // TODO: use buildString { append(...) } — receiver is a StringBuilder
//sampleEnd Show answer Hide answer
fun greeting(): String = buildString {
append("Hello, ")
append("world")
}Inside the block, append needs no prefix because the lambda has a receiver (this is the StringBuilder). That receiver mechanism is the whole basis of apply, the scope functions, and Kotlin DSLs.
Back to the lesson, Why Lambdas Are Free. That completes the lambda series — next in the curriculum: classes.
Comments