RizTech Academy logo
RizTech Academy
Writing Idiomatic KotlinLesson 3 of 530 min

Scope functions and the idioms that mark a Kotlin developer

There is Kotlin that a Java developer writes on day one, and Kotlin that a Kotlin developer writes. The difference is idiom — using the language the way it wants to be used. This lesson gathers the idioms that mark experience, with the scope functions at the centre, because they are the most powerful and the most abused feature a newcomer meets.

The scope functions: let, run, with, apply, also

Kotlin has five functions that run a block "in the context of" an object. They are genuinely useful and genuinely overused into unreadable code, so it is worth learning what each is for rather than scattering them by feel.

They differ on two axes: how you refer to the object (it or this) and what they return (the object itself, or the block's result).

// apply — configure an object, return the object. `this` refers to it.
val user = User().apply {
    name = "Kavita"          // `this` is implicit — just set properties
    email = "k@x.com"
}   // returns the configured User

// also — do a side effect with the object, return the object. `it` refers to it.
val numbers = mutableListOf(1, 2, 3).also {
    println("Created list of size ${it.size}")   // log, then carry on
}   // returns the list

// let — transform the object into something else, return the result. `it` refers to it.
val length = name?.let { it.trim().length }      // null-safe transform

// run — like let but `this`, return the block's result.
val area = rectangle.run { width * height }      // compute from the object's properties

// with — like run but not an extension; return the block's result.
val summary = with(user) { "$name <$email>" }    // read several properties

The way to choose, rather than memorise a table:

  • apply — "configure this and give it back". Object-building. Returns the object.
  • also — "do something on the side (log, validate) and give it back". Returns the object.
  • let — "turn this into something else", especially null-safely (?.let). Returns the result.
  • run / with — "compute a result from this object's members". Return the result.

The honest guidance: use them sparingly, and never nest them. A single apply to build an object or a single ?.let to null-safely transform is clear. Three scope functions nested inside each other, with it and this meaning different things at each level, is a puzzle nobody thanks you for. The test is the same as always: does it read better than a plain if or a named local val? Often a boring val trimmed = name.trim() beats a clever name.let { ... }. Reach for a scope function when it genuinely clarifies; otherwise write the plain version.

Prefer expressions to statements

Kotlin's if, when, and try are expressions (they return values), so assign their result rather than reassigning a var:

// statement style — a var, set in branches
var label: String
if (score >= 50) label = "pass" else label = "fail"

// expression style — a val, obviously
val label = if (score >= 50) "pass" else "fail"

The expression version is a val (cannot be left unset or later mangled), and the intent is plain. This applies to when too, and it is one of the clearest signals of idiomatic Kotlin: a Kotlin developer assigns expressions; a Java transplant reassigns vars.

Data classes over tuples and maps

When you need to pass a few related values together, make a data class, not a Pair, Triple, or a Map<String, Any>:

// unclear — what are .first and .second?
fun parseName(full: String): Pair<String, String> = ...
val p = parseName("Kavita Joshi")
println(p.first)     // is .first the first name or the last?

// clear — named fields
data class Name(val first: String, val last: String)
fun parseName(full: String): Name = ...
val n = parseName("Kavita Joshi")
println(n.first)     // obviously the first name

Pair and Triple save you a declaration and cost every reader the meaning. A one-line data class names the fields, and the call site reads. Reserve Pair for genuinely throwaway local use.

Small idioms that add up

A handful more that mark fluency:

  • when over long if/else if chains — clearer, and exhaustive on sealed types (module 6).
  • String templates over concatenation — "$name has $count orders", not name + " has " + ....
  • ?: for defaults and early exits — val name = input ?: return, not a nested if.
  • Destructuring where the shape is obvious — val (name, city) = customer.
  • Trailing lambdas and it for short, clear higher-order calls — list.filter { it > 0 }.
  • Named arguments for clarity at call sites with several arguments.
  • require/check for validation — require(qty > 0) { "qty must be positive" } — instead of hand-rolled if (...) throw.

None of these is dramatic on its own. Together they are the difference between Kotlin that reads like Kotlin and Kotlin that reads like translated Java.

The meta-rule: idiomatic means clearer, not cleverer

The trap in a lesson like this is to treat "idiomatic" as a licence to be clever — to cram a function into a single nested chain of scope functions and operators because you can. That is the opposite of the goal. Every idiom here exists to make code clearer, and the moment an idiom makes code harder to read, the idiom is wrong for that spot. The most experienced Kotlin developers write remarkably plain code: an apply here, a ?.let there, an expression when, and otherwise straightforward statements with good names. Reach for the fancy tool only when it genuinely reads better than the plain one. Clarity always wins.

Check your work

What the scope functions do. Run a block in the context of an object, differing on it versus this and on returning the object versus the block's result.

Which returns the object. apply (with this) and also (with it) — for configuring and side effects.

Which returns the block's result. let (with it), run and with (with this) — for transforming or computing.

The guidance on scope functions. Use sparingly, never nest, and only when clearer than a plain if or a named val.

Expression versus statement style. Assign an if/when/try expression to a val rather than reassigning a var in branches.

Data class versus Pair/Triple. A data class names the fields; Pair costs the reader the meaning — reserve it for throwaway use.

Small idioms that mark fluency. when over if/else if, string templates, ?:, destructuring, trailing lambdas, named arguments, require/check.

The meta-rule. Idiomatic means clearer, not cleverer — the moment an idiom hurts readability, it is wrong there.

Practice

  1. Build an object with apply (setting several properties), and log it on the side with also. Confirm each returns the object.
  2. Transform a nullable value with ?.let and compare with the plain if (x != null) version. Decide which is clearer for a one-liner and for a five-line block.
  3. Take a nested pair of scope functions and rewrite it with plain named vals. Judge which reads better.
  4. Rewrite a var-set-in-branches into an expression val with if and with when.
  5. Replace a function returning Pair<String, String> with one returning a named data class.
  6. Convert three concatenated strings to a string template, and one hand-rolled validation to require.
  7. Find the "cleverest" line in your code and rewrite it plainly. Decide honestly which you would rather maintain.

Official documentation

Next: error handling — exceptions, nullability, and Result.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship