HomebaseWorks
Lesson 4 of 4

Scope functions, read-only collections, ranges and lazy

Pick between let, apply, also, run and with, know when a list is really mutable, loop over ranges correctly and defer work with lazy.

EWritten by the HomebaseWorks editorial team · reviewed 26 Oct 2026

Kotlin code reviews and screenings keep returning to a few idioms: scope functions, the difference between List and MutableList, range loops and lazy initialisation. Each is small, but using the wrong one makes code harder to read, and interviewers notice.

Advertisement

The five scope functions

All five run a block on an object. They differ in how the object is named inside the block (it or this) and in what they return (the object or the block's result).

class Report(var title: String = "", var pages: Int = 0)

val r = Report().apply {          // this; returns the object
    title = "Q3"
    pages = 12
}
val length = r.title.let { it.length }          // it; returns block result -> 2
val input: String? = null
val shown = input?.let { "Got $it" } ?: "nothing"   // nothing
  • apply: configure an object, return it.
  • also: side effect such as logging, return the object.
  • let: transform a value, often with ?. for null handling, return the result.
  • run and with: compute a result from an object's members; run works on nullable receivers with ?..

Read-only versus mutable collections

val names = listOf("Ravi", "Asha")          // List: no add or remove
val editable = mutableListOf("Ravi")
editable.add("Asha")
println(names.firstOrNull { it.startsWith("Z") })   // null
println((1..5).filter { it % 2 == 1 }.map { it * 10 })   // [10, 30, 50]

val stops the variable from being reassigned; it does not make the list read-only. listOf returns a List interface with no mutating methods, which is what you expose from a function. Keep MutableList private and hand out List.

Ranges and lazy

for (i in 1..3) print(i)                    // 123
for (i in 10 downTo 0 step 5) print("$i ")  // 10 5 0

val lazyValue: String by lazy {
    println("computing")
    "done"
}
println(lazyValue)   // computing, then done
println(lazyValue)   // done

.. includes both ends, until excludes the last value, so 0 until list.size is the safe index loop. by lazy runs the block once, on first access, and caches the result; it is for val. lateinit var is for non-null properties set later by a framework, and reading it before assignment throws.

Common mistakes and a practice habit

  • Using apply when you need the block's result, or let when you only wanted to configure the object.
  • Writing 0..list.size and reading one past the end.

Practice habit: Take a function from your project that builds an object step by step and rewrite it with apply; then take a nullable lookup and rewrite it with ?.let and ?:. Read both versions aloud. If the rewritten one is not clearly shorter and clearer, keep the original. That judgement is the skill being tested.

Advertisement
Continue with Kotlin4 lessons · 1 interview set · 1 test · 1 certificate
Not tested

More in Programming