How do I choose the right scope function for different use cases in Kotlin?

Kotlin scope functions all do the same broad thing: they execute a block of code in the context of an object. The main differences are:

  1. How you refer to the object: this or it
  2. What the function returns: the object itself or the block result

Quick decision table

Function Object reference Returns Best for
let it Lambda result Transforming a value, null-safe execution
run this Lambda result Computing a result from an object
with this Lambda result Grouping operations on an existing non-null object
apply this The original object Configuring or initializing an object
also it The original object Side effects like logging, validation, debugging

Use let when you want to transform a value

let is useful when you want to take an object and produce another value.

val name = "kotlin"

val length = name.let {
    it.uppercase().length
}

println(length) // 6

It is also very common with nullable values:

val email: String? = "[email protected]"

email?.let {
    println("Sending email to $it")
}

Use let when you are thinking:

“If this value exists, do something with it or turn it into something else.”


Use apply when you want to configure an object

apply returns the original object, so it is ideal for initialization.

data class User(
    var name: String = "",
    var age: Int = 0,
    var active: Boolean = false
)

val user = User().apply {
    name = "Alice"
    age = 30
    active = true
}

Inside apply, the object is available as this, so you can access its properties directly.

Use apply when you are thinking:

“Create this object and set it up.”


Use also when you want side effects without changing the chain result

also returns the original object, like apply, but the object is referenced as it.

This makes it good for logging, debugging, validation, or extra actions.

val numbers = mutableListOf(1, 2, 3)
    .also {
        println("Original list: $it")
    }
    .apply {
        add(4)
    }
    .also {
        println("Updated list: $it")
    }

println(numbers) // [1, 2, 3, 4]

Use also when you are thinking:

“Do this extra thing, but keep passing the same object along.”


Use run when you want to compute a result using an object

run uses this and returns the lambda result.

val message = StringBuilder().run {
    append("Hello, ")
    append("Kotlin")
    toString()
}

println(message) // Hello, Kotlin

This is useful when you want to perform multiple operations and return a final computed value.

Use run when you are thinking:

“Use this object to calculate a result.”


Use with when you already have an object and want to group operations

with is not called as an extension in the same style as the others. You pass the object as an argument.

val builder = StringBuilder()

val result = with(builder) {
    append("Hello, ")
    append("Kotlin")
    toString()
}

println(result) // Hello, Kotlin

Use with when you are thinking:

“With this existing object, do several things.”

with is usually best for non-null objects. For nullable values, prefer ?.let or ?.run.


The simplest way to choose

Ask two questions:

1. Do you want to return the original object?

If yes:

  • Use apply for configuration
  • Use also for side effects
val user = User().apply {
    name = "Alice"
}
val user = getUser().also {
    println("Loaded user: $it")
}

2. Do you want to return a new result?

If yes:

  • Use let when you prefer it
  • Use run when you prefer this
  • Use with when the object already exists and is non-null
val length = name.let {
    it.length
}
val text = builder.run {
    append("Done")
    toString()
}

Rule of thumb

let   = transform or null-check
run   = compute a result using this object
with  = operate on an existing object
apply = configure an object
also  = perform side effects

Common examples

Null-safe call

val username: String? = "sam"

username?.let {
    println("Username length: ${it.length}")
}

Best choice: let


Object setup

val request = Request().apply {
    method = "GET"
    url = "/users"
}

Best choice: apply


Logging in a chain

val result = loadUsers()
    .also { println("Loaded ${it.size} users") }
    .filter { it.active }

Best choice: also


Build a value from several operations

val summary = users.run {
    val activeCount = count { it.active }
    "Active users: $activeCount"
}

Best choice: run


Group repeated calls on one object

val result = with(StringBuilder()) {
    append("A")
    append("B")
    append("C")
    toString()
}

Best choice: with


Avoid overusing scope functions

Scope functions are useful, but chaining too many can make code harder to read:

val result = user
    .also { println(it) }
    .let { transform(it) }
    .also { save(it) }
    .run { toDto() }

Sometimes plain code is clearer:

println(user)

val transformed = transform(user)
save(transformed)

val result = transformed.toDto()

A good guideline is: use scope functions when they make intent clearer, not just to make code shorter.

How do I use the this, super, and @ labels for disambiguation in Kotlin class hierarchies?

In Kotlin, this, super, and @ labels are used to disambiguate which receiver or superclass member you mean, especially in nested scopes, inheritance, and inner classes.

1. this: refer to the current receiver

Inside a class, this refers to the current instance of that class.

class User(val name: String) {
    fun printName() {
        println(this.name)
    }
}

Usually this is optional:

println(name)

is the same as:

println(this.name)

2. this@Label: choose a specific outer receiver

When you have nested classes, lambdas, or extension functions, there may be multiple possible this receivers. Kotlin lets you qualify this with a label.

Class receiver

class Outer {
    val name = "Outer"

    inner class Inner {
        val name = "Inner"

        fun printNames() {
            println(this.name)        // Inner
            println([email protected])  // Inner
            println([email protected])  // Outer
        }
    }
}

this@Outer explicitly means “the this of Outer”.


3. Labels in lambdas

You can label lambdas and then use this@label to access that lambda’s receiver.

class Html {
    fun body() {
        println("body")
    }
}

fun html(block: Html.() -> Unit) {
    Html().block()
}

fun main() {
    html outer@ {
        this.body()
        [email protected]()
    }
}

Here:

this@outer

refers to the receiver of the lambda labeled outer.


4. super: call superclass implementation

Use super to access a member from the immediate superclass.

open class Parent {
    open fun greet() {
        println("Hello from Parent")
    }
}

class Child : Parent() {
    override fun greet() {
        super.greet()
        println("Hello from Child")
    }
}

Output:

Hello from Parent
Hello from Child

5. super<Type>: disambiguate multiple inherited implementations

If a class inherits the same member from multiple supertypes, you must specify which one to call.

interface A {
    fun greet() {
        println("Hello from A")
    }
}

interface B {
    fun greet() {
        println("Hello from B")
    }
}

class C : A, B {
    override fun greet() {
        super<A>.greet()
        super<B>.greet()
        println("Hello from C")
    }
}

Here:

super<A>.greet()
super<B>.greet()

select the specific supertype implementation.


6. super@Label: access an outer class’s superclass

In inner classes, super normally refers to the superclass of the inner class. If you need the superclass of an outer class, use a qualified super.

open class Base {
    open fun message() {
        println("Base")
    }
}

open class OuterBase : Base() {
    override fun message() {
        println("OuterBase")
    }
}

class Outer : OuterBase() {
    override fun message() {
        println("Outer")
    }

    inner class Inner {
        fun callOuterSuper() {
            [email protected]()
        }
    }
}

Here:

[email protected]()

means “call the superclass implementation of Outer”.

So this calls:

OuterBase.message()

not Outer.message().


7. Combining super<Type>@Label

If the outer class implements multiple supertypes, you can combine both forms.

interface A {
    fun print() {
        println("A")
    }
}

interface B {
    fun print() {
        println("B")
    }
}

class Outer : A, B {
    override fun print() {
        println("Outer")
    }

    inner class Inner {
        fun callOuterSupers() {
            super<A>@Outer.print()
            super<B>@Outer.print()
        }
    }
}

Here:

super<A>@Outer.print()
super<B>@Outer.print()

means:

  • call A’s implementation as inherited by Outer
  • call B’s implementation as inherited by Outer

Summary

Syntax Meaning
this Current receiver
this@Outer this of a specific labeled or outer receiver
super Immediate superclass implementation
super<Type> Specific superclass or interface implementation
super@Outer Superclass implementation of an outer class
super<Type>@Outer Specific supertype implementation of an outer class

In short:

this@Something

chooses which object/receiver you mean.

super<Something>

chooses which superclass/interface implementation you mean.

super<Something>@Outer

chooses which supertype implementation of which outer receiver you mean.

How do I use the “this” keyword in Java?

Every instance method has a variable with the name this that refers to the current object for which the method is being called. You can refer to any member of the current object from within an instance method or a constructor by using this keyword.

Each time an instance method is called, the this variable is set to reference the particular class object to which it is being applied. The code in the method will then relate to the specific members of the object referred to by this keyword.

package org.kodejava.basic;

public class RemoteControl {
    private String channelName;
    private int channelNum;
    private int minVolume;
    private int maxVolume;

    RemoteControl() {
    }

    RemoteControl(String channelName, int channelNum) {
        // use "this" keyword to call another constructor in the 
        // same class
        this(channelName, channelNum, 0, 0);
    }

    RemoteControl(String channelName, int channelNum, int minVol, int maxVol) {
        this.channelName = channelName;
        this.channelNum = channelNum;
        this.minVolume = minVol;
        this.maxVolume = maxVol;
    }

    public static void main(String[] args) {
        RemoteControl remote = new RemoteControl("ATV", 10);

        // when the following line is executed, the variable in
        // changeVolume() is referring to remote object.
        remote.changeVolume(0, 25);
    }

    public void changeVolume(int x, int y) {
        this.minVolume = x;
        this.maxVolume = y;
    }
}