Kotlin’s standard library offers many functions, but the ones that stand out in my opinion are scope functions. These scope functions exist to execute a code block in the context of the object on which you execute it. Within this scope, you can perform operations on the object with or without a name. Scope functions can make code more readable and concise, but beware! Nesting scope functions can cause confusion about the current context object.

Kotlin provides the following five scope functions:

  • Apply

  • Let

  • With

  • Run

  • Also

Each of these scope functions has a purpose, and it can sometimes be confusing to know which scope function to use. In this blog, I will describe each of the scope functions and what they are used for.


Context object reference: this
Return value: context object
Signature: <T> T.apply(block: T.() → Unit): T

val user = User().apply {
    this.name = "John Driven"
    email = "johndriven@drivenj.com"
    password = "unencrypted_password"

User(name='John Driven', email='johndriven@drivenj.com', password='unencrypted_password')

Here I call the apply function on the User object and configure the User within the scope of the apply function. This avoids the need to call the set functions with user. prefixes. As you can see you can either use this or leave it. This function is mainly used when configuring objects and is useful when a (Java) object does not have a builder to configure the object. Pretty straightforward, now on to let.


Context object reference: it
Return value: lambda result
Signature: <T, R> T.let(block: (T) → R): R

val users = listOf(User("John", 20), User("Jenny", 56), User("Joe", 65))
val cell = users.map { it.age }.filter { it > 50 }.size
    .let {
        print("Amount of users above 50 $it")
        writeToCsv(userAmountHeader, it)
Output: CsvRow(header = 'user_amount', value = '2')

I have a list of users and I want to find out the number of users who are over fifty. Once I have the number of users, I can call the let function to call multiple functions on it (the number). The let function is similar to the map function. However, instead of performing it on a collection, you perform it on an object. This is also the main use of let, to be able to call multiple functions on the context object. The next one I am going to cover is with.


Context object reference: this
Return value: lambda result
Signature: <T, R> with(receiver: T, block: T.() → R): R

val calculation = with(Calculator()) {

Output: 95

With is a bit of an odd duck among the scope functions, because it is not an extension function. The context object is a parameter, but in a lambda, and it can be called with or without it. Calculator is scoped within the with function code block and it will not leak outside of that block. The next one I will cover is the run function.


Context object reference: this
Return value: lambda result
Signature <T, R> T.run(block: T.() → R): R

val repository = Repository("jdbc:mysql://mysql.db.server:3306/database")
val result = repository.run {
    table = "users"
    executeQuery(prepareRequest() + " for table $table.")

Output: Query executed: 'SELECT COUNT(*) FROM users;' with result 10 for table users.

In this case we call run on the repository object, which configures the table and then executes a query on it. Run is therefore useful for both initialising objects and returning them. And at last the also function.


Context object reference: it
Return value: context object Signature: <T> T.also(block: (T) → Unit): T

val user = User("John", 10).also {
    println("Current: $it")
    it.name = "Joe"
    println("Modified: $it")
Current: User(name=John, age=10)
Modified: User(name=Joe, age=10)

The also function can be read as "also do the following with the object". Also is mainly used for actions for which you want the whole object as a reference, as opposed to the properties or functions of the context object.

I hope this has helped you understand how to use Kotlin’s scope functions. In particular, when to use which scope function.