Kotlin scope functions interview questions, with answers
Kotlin's five scope functions, let, run, with, apply and also, all do the same basic thing: run a block of code with an object in scope. They differ in two details, how the block refers to the object and what the call returns, and those details are exactly what interviewers ask about, because choosing the wrong one is a common source of subtle bugs.
The answers below go through each function and when to use it, with code compiled with kotlinc 2.3 and run on the JVM. Then take the free Kotlin diagnostic — ten questions across every Kotlin topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What are Kotlin's scope functions?
In short: let, run, with, apply and also run a block with an object in scope; they differ in whether the object is it or this and whether they return the object or the block's result.
Two questions tell them apart. How is the object referred to inside the block: as it, in let and also, or as this, in run, with and apply, where its members can be called without a prefix? And what does the call return: the block's last expression, for let, run and with, or the object itself, for apply and also? So "go".let { it.length } returns 2, while StringBuilder().apply { append("hi") } returns the builder, which toString then turns into hi. with is the only one that is not an extension: it takes the object as an argument.
val n = "go".let { it.length } val s = StringBuilder().apply { append("hi") }.toString() println("$n $s") // 2 hi
2.When should you use let in Kotlin?
In short: Use let to run code only when a nullable value is not null, with ?.let, or to transform a value and return the result.
The most common use is x?.let { ... }: the block runs only when x is not null, and inside it, it is the non-null value, so no further checks are needed, as with email below. let is also useful for introducing a short-lived name for the result of an expression in the middle of a chain, and for turning one value into another, since it returns the block's result. Deeply nested lets hurt readability; an early return with ?: return or a plain if is often clearer. Naming the parameter, email?.let { e -> ... }, helps when blocks are nested.
val email: String? = "a@x.in" email?.let { println(it.uppercase()) } // A@X.IN
3.What is the difference between apply and also in Kotlin?
In short: Both return the object itself; apply refers to it as this, which suits configuring it, and also refers to it as it, which suits side effects such as logging.
apply runs the block with the object as this, so property assignments and method calls inside it need no prefix, which makes it the idiom for configuring a new object, as add(2) is below. also passes the object as it and suits actions performed on the object rather than on its members, such as printing, validating or registering it. Because both return the object unchanged, they can be inserted in the middle of a chain without affecting it: the chain below still produces the list, and also prints its size along the way.
val list = mutableListOf(1) .apply { add(2) } .also { println(it.size) } // 2 println(list) // [1, 2]
4.What is the difference between run and with in Kotlin?
In short: Both run a block with the object as this and return the block's result; run is called on the object, x.run { }, while with takes it as an argument, with(x) { }.
sb.run { length * 10 } and with(sb) { length * 10 } do the same thing: inside the block this is sb, and the value of the last expression is returned. The difference is only in calling style. run is an extension, so it chains and works with the safe call, x?.run { ... }, which with cannot do. with reads naturally when an object is used for several operations in a row, as with(sb) { reverse() } below; reverse changes sb and returns it, so r2 prints as ba. A run without a receiver also exists, simply running a block where an expression is expected.
val sb = StringBuilder("ab") val r1 = sb.run { length * 10 } val r2 = with(sb) { reverse() } println("$r1 $r2") // 20 ba
5.What do takeIf and takeUnless do in Kotlin?
In short: takeIf returns the object if a predicate is true and null otherwise; takeUnless does the opposite, which makes them easy to combine with ?. and ?:.
age.takeIf { it > 17 } yields age when the condition holds and null when it does not, turning a check into a nullable value. Combined with the Elvis operator it gives a compact conditional, as in ok ?: "minor" below, and combined with ?.let it runs a block only for values that pass a test, such as file.takeIf { it.exists() }?.readText(). takeUnless inverts the predicate. The predicate is always evaluated, so a condition with side effects runs even when the result is discarded, and long conditions are clearer as an ordinary if.
val age = 17 val ok = age.takeIf { it > 17 } println(ok ?: "minor") // minor
6.How do you choose between let, run, with, apply and also?
In short: Pick by what you need back and how you will refer to the object: the object itself (apply, also) or a result (let, run, with), and this for member access or it for passing it on.
The official guidance reduces to a few cases. To configure an object and keep it, use apply. To perform an extra action with an object in a chain, such as logging, use also. To run code on a non-null value, use ?.let, and to compute a result from an object, use let if the block mostly passes the object to other functions, or run if it mostly calls the object's own members. Use with for several calls on one object when no chaining is needed. When none of these makes the code clearer than a local variable and an if, prefer the plain version; scope functions are for readability, not for their own sake.
How the diagnostic asks it
One question from the Kotlin bank, exactly as a sitting would show it. The bank has 3 on scope functions and 30 across Kotlin.
What does this Kotlin code print?
val a = 5.let { it * 2 } val b = 5.also { it * 2 } println("$a $b")
- 110 10
- 25 10
- 35 5
- 410 5correct
let and also both pass the object to the lambda as it, but they return different things: let returns the lambda's result, so a is 5 * 2 = 10, and also returns the object it was called on, ignoring the lambda's result, so b is 5. The output is 10 5. 10 10 assumes they behave alike. 5 10 swaps them. 5 5 assumes neither uses the lambda's value. also is meant for side effects, such as logging, in the middle of a chain, where the value should pass through unchanged; let is for transforming a value, often a nullable one with ?.let.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Kotlin questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.