iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

let / run / apply / also / with

Kotlin’s scope functions (let, run, with, apply, also) execute a block in the context of an object. Picking the right one comes down to two questions: how should the receiver be referenced (this or it) and what should the block return (the object or the block result)?

let, run, with, apply, also, decision

EXAMPLE
// 1) Cheat sheet
//   apply  — receiver as 'this', returns the receiver  → configure-this object
//   also   — receiver as 'it',   returns the receiver  → side effects (log, peek)
//   run    — receiver as 'this', returns block result  → transform with member access
//   let    — receiver as 'it',   returns block result  → null-safety + transformation
//   with   — receiver as 'this', returns block result  → group calls on one object (not extension)

// 2) apply — common builder-style
data class User(var name: String = "", var email: String = "", var role: String = "")

val user = User().apply {
    name  = "Mara"
    email = "mara@example.com"
    role  = "admin"
}

// Useful for stamping config on an object you've just constructed.

// 3) also — side effects that should not change the value
val saved = repository.save(user).also {
    logger.info("saved user ${it.id}")
}

// 'also' returns the receiver, so the chain continues with the original object.
// Naming it 'it' makes the side-effect intent clear.

// 4) let — null safety + scope
val emailLength = user.email?.let { it.length } ?: 0

fun greet(name: String?) {
    name?.let { println("Hello, $it") }
}

// let is ALSO the way to introduce a local val with a transformation:
val doubled = numbers.map { it * 2 }
    .let { it.average() }                  // converts list → double; available in the chain

// 5) run — block result, with member-access via 'this'
val summary = user.run {
    "$name <$email> (${role.uppercase()})"
}

// Useful when you want to use the object's members WITHOUT prefixing each access.

// Run can also be called without a receiver — used for grouping setup into a single expression
val port = run {
    val raw = System.getenv("PORT")
    raw?.toIntOrNull() ?: 8080
}

// 6) with — when the object is a parameter (not an extension)
val config = with(loadConfig()) {
    "host=$host port=$port db=$db"
}

// 'with' is a regular function (not extension); reads naturally for one-off grouping.

// 7) Side-by-side comparison
class Order(var id: String = "", var amount: Long = 0L)

val o1 = Order().apply { id = "o-1"; amount = 1000 }              // returns Order
val o2 = Order().also  { it.id = "o-2"; it.amount = 2000 }         // returns Order
val s1 = Order().run    { "$id: $amount" }                       // returns String
val s2 = Order().let    { "${it.id}: ${it.amount}" }              // returns String
val s3 = with(Order())  { "$id: $amount" }                       // returns String

// 8) Decision tree
//   need to RETURN the same object?
//     yes → use receiver as 'this' (mutating many props)?  apply
//                                  side effect, peek?      also
//   need to RETURN the BLOCK result?
//     yes → access members easily as 'this'?               run
//          handle null with ?. + transform?                 let
//          object isn't an extension target (e.g. constructed inline)? with

// 9) Chaining safely
user?.takeIf { it.active }?.let { /* only runs if non-null and active */ }
    ?.also { audit.recordRead(it.id) }
    ?.run  { "$name ($role)" }
    ?.also { println(it) }

// 10) Idiomatic Android usage
val intent = Intent(this, DetailActivity::class.java).apply {
    putExtra("id", productId)
    putExtra("source", "feed")
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)

binding.title.text = product.run { "${title} — ${formattedPrice()}" }

// 11) Server-side example
fun ApplicationCall.respondError(status: HttpStatusCode, message: String) =
    response.status(status).also {
        respond(HttpStatusCode.fromValue(status.value), mapOf("error" to message))
    }

// 12) Patterns to AVOID
// • Nesting scope functions 3+ deep — refactor into named functions
// • Using 'apply' just because — if you don't need the receiver back, prefer 'run' or 'let'
// • Switching between 'this' and 'it' to make the line shorter — readability beats brevity
// • Naming a real receiver 'it' inside a long block — name it explicitly: .let { user -> … }
// • Using 'with' on long expressions — assign to a val first or use the extension form (apply/run/also)

// 13) Null handling — let beats safe-call chains
val token = request.headers["Authorization"]
    ?.removePrefix("Bearer ")
    ?.takeIf { it.isNotBlank() }
    ?.let { tokens.verify(it) }
    ?: return Unauthorized

// 14) Mixed usage in a constructor / DSL
class Query private constructor() {
    var select: List<String> = emptyList()
    var from:   String       = ""
    var where:  String?      = null

    companion object {
        fun build(block: Query.() -> Unit): Query = Query().apply(block)
    }
}

val q = Query.build {
    select = listOf("id", "name")
    from   = "users"
    where  = "active = true"
}

// 15) Performance note
// All five scope functions are 'inline' — at compile time they unfold into regular blocks with no
// allocations. Performance is the same as writing the code by hand. Pick by readability, not perf.

// 16) Common bugs
//   • Mixing up apply and also — apply is 'this', also is 'it'; readers can't tell at a glance which property is being set
//   • Using let on a non-nullable receiver — the safe-call is the value; let is for the conditional/transform aspect, not the null check
//   • Returning the wrong value from run/with — common when adding statements after the result
//   • Naming the parameter 'it' inside a long block — rename explicitly when scope grows
//   • Apply chain modifying the object then forgetting that side-effecting calls don't update 'it' in 'also' — easy to spot in code review

Why it matters

Choose scope functions by what they return (apply/also return the receiver, run/let/with return the block) and how the receiver is referenced (this for apply/run/with, it for let/also). apply for builder-style config, also for logging side effects, let for null safety, run for terse derivations.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
user.let { println(it.name) }
user.also { println("created $it") }
user.apply { age = 37 }                // modify, return user
val msg = user.run { "$name ($age)" }  // compute from user
Try it Yourself »

Exercise

Run a block and return its receiver modified.

user. { age = 37 }

Discussion

Loading…