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() }
Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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? T

reified 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).

Try it — edit, then press Run 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".

Try it — edit, then press Run 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.

Try it — edit, then press Run 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".

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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?

Try it — edit, then press Run 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.

Implement it, then press Run to check 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".

Implement it, then press Run to check 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