Scope Functions Workbook

Ten short exercises on Kotlin's scope functions — let, run, with, apply, and also, decided by receiver-versus-parameter and result-versus-object, plus takeIf and takeUnless.

Practice problems for let, run, apply, also, with. Each takes a minute or two, and each comes with a runnable editor — fill in the TODO, press Run to compile it 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.

the two questions

1. Configure and return

Create a StringBuilder, append "a" and "b", and keep the builder — using the scope function meant for configuration.

Try it — edit, then press Run fun main() { val sb = StringBuilder() // TODO: use apply to append "a" then "b", keeping the builder println(sb.toString()) // want ab }
Show answer Hide answer
val sb = StringBuilder().apply {
    append("a")
    append("b")
}

apply: receiver is this, returns the object.

2. Do this if not null

Given name: String?, send a welcome to it only when it isn’t null.

Try it — edit, then press Run fun main() { fun sendWelcome(n: String) = println("Welcome, " + n) val name: String? = "Ada" // TODO: call sendWelcome only when name isn't null, using name?.let println("name = " + name) }
Show answer Hide answer
name?.let { sendWelcome(it) }

let: object is it, returns the block’s result.

3. Side effect in a chain

Given loadUser(), log the user and return it unchanged, in one chained call.

Try it — edit, then press Run fun main() { fun loadUser() = "Ada" // TODO: log the user and return it unchanged, using loadUser().also { ... } val user = loadUser() println(user) }
Show answer Hide answer
loadUser().also { println("loaded $it") }

also: object is it, returns the object.

4. Compute from members

Given a rectangle with width and height, compute its area with a scope function that exposes the object as this and returns the result.

Try it — edit, then press Run fun main() { val rectangle = object { val width = 4 val height = 3 } val area = 0 // TODO: compute width * height with run (object exposed as this) println(area) // want 12 }
Show answer Hide answer
val area = rectangle.run { width * height }

5. Group operations on one object

Given canvas, call drawLine(...) and drawCircle(...) on it without repeating canvas, using the non-extension form.

Try it — edit, then press Run fun main() { class Canvas { fun drawLine(x0: Int, y0: Int, x1: Int, y1: Int) = println("line") fun drawCircle(x: Int, y: Int, r: Int) = println("circle") } val canvas = Canvas() // TODO: call both on canvas via with(canvas) { ... }, no repeated 'canvas' canvas.drawLine(0, 0, 10, 10) canvas.drawCircle(5, 5, 3) }
Show answer Hide answer
with(canvas) {
    drawLine(0, 0, 10, 10)
    drawCircle(5, 5, 3)
}

takeIf and choosing

6. Keep it only if it qualifies

Given input: String, return it only if it’s not blank, otherwise return from the function.

Try it — edit, then press Run fun main() { val input = "hello" val value = input // TODO: keep it only if not blank, else return (takeIf + Elvis) println(value) // want hello }
Show answer Hide answer
val value = input.takeIf { it.isNotBlank() } ?: return

takeIf returns the object or null, so it chains with Elvis.

7. Keep unless

Given input: String, return it unless it’s blank (in which case null), using a single scope function.

Try it — edit, then press Run fun main() { val input = "hello" val value = input // TODO: keep it unless blank (null if blank), using takeUnless println(value) // want hello }
Show answer Hide answer
input.takeUnless { it.isBlank() }

takeUnless is takeIf’s negation — it keeps the object only when the condition is false.

8. Build and assign

Create a mutableListOf<Int>(), add 1 and 2 to it, and assign the resulting list to nums — using the scope function that returns the object.

Try it — edit, then press Run fun main() { val nums = mutableListOf<Int>() // TODO: add 1 and 2 with apply, keeping the list println(nums) // want [1, 2] }
Show answer Hide answer
val nums = mutableListOf<Int>().apply {
    add(1)
    add(2)
}

apply returns the object (the list), so the whole expression is assignable; also is its it-based counterpart.

9. Swap this for it

Rewrite rectangle.run { width * height } using let instead, so the object is referred to as it.

Try it — edit, then press Run fun main() { val rectangle = object { val width = 4 val height = 3 } val area = rectangle.run { width * height } // TODO: rewrite with let (it.width * it.height) println(area) // want 12 }
Show answer Hide answer
rectangle.let { it.width * it.height }

run exposes the object as this; let exposes it as it.

10. Combine two in a chain

Given a fresh Request(url), set its method = "POST" and then log it — using apply then also in a single chain that ends holding the request.

Try it — edit, then press Run fun main() { class Request(val url: String) { var method: String = "GET" } val url = "http://example.com" // TODO: set method = "POST" with apply, then log with also, keeping the request val request = Request(url) println(request.method) // want POST }
Show answer Hide answer
val request = Request(url)
    .apply { method = "POST" }
    .also { println("prepared $it") }

apply configures via this; also runs a side effect via it. Both return the object, so the chain ends with the request.


Going deeper: apply and takeIf

12. Configure and return

Use apply to build a StringBuilder, append first name, a space, and last name, and return the string. apply exposes the object as this and returns it.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun buildName() { Assert.assertEquals("Ada Lovelace", buildName("Ada", "Lovelace")) } } //sampleStart fun buildName(first: String, last: String): String = StringBuilder().toString() // TODO: use apply { append(...) } then .toString() //sampleEnd
Show answer Hide answer
fun buildName(first: String, last: String): String =
    StringBuilder().apply {
        append(first); append(" "); append(last)
    }.toString()

apply returns the configured StringBuilder, and the receiver is this, so append needs no prefix.

13. Gate a value

Use takeIf so blankToNull returns the string when it’s non-blank, or null otherwise.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun blankToNull() { Assert.assertEquals("x", blankToNull("x")) Assert.assertNull(blankToNull(" ")) } } //sampleStart fun blankToNull(s: String): String? = s // TODO: return s only if it isn't blank, else null //sampleEnd
Show answer Hide answer
fun blankToNull(s: String): String? = s.takeIf { it.isNotBlank() }

takeIf returns the object when the predicate holds and null otherwise — a predicate turned into a nullable you can chain with ?: or ?.let.

14. Which one, and why not nest?

You want to configure a freshly-created object and get it back — which scope function? And why is nesting apply/run a hazard? Reveal to check.

Show answer Hide answer

apply — it exposes the object as this and returns the object (let returns the block’s result and uses it; the pair of choices “this vs it” and “object vs result” is the whole map). Nesting is a hazard because inside a nested apply/run, an unqualified name resolves against the innermost receiver first, so name = "x" can silently target the wrong object — disambiguate with a label like [email protected], or just don’t nest.


Back to the lesson, let, run, apply, also, with, or on to the next one: operator overloading.

Comments