Scope Functions

The Merriam-Webster dictionary defines the word scope as:

1: INTENTION, OBJECT
2: space or opportunity for unhampered motion, activity, or thought
3: the extent of treatment, activity, or influence
4: range of operation

This makes a lot of sense when we refer to scope in programming. The scope of a variable is the “area” where that variable can be used, for example.

Scope functions are “special” functions from the Kotlin standard library whose sole purpose is to execute a block of code within the context (scope) of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name.

The most common scope functions provided are let, run, with, apply, and also.

They usually help you write more expressive (this is debatable in my opinion) and concise (A-HA, here it is AGAIN) code.

The scope function is usually called on a value/object, commonly known as the receiver, and it takes in a lambda that defines what you want to do with that value. They pretty much all follow this rule: execute a block of code on an object. What’s different is how this object becomes available inside the block and what is the result of the whole expression.

Before we dig into each of them, let’s clarify a few things.

Context object: this or it

Inside the lambda of a scope function, the context object is available by a short reference instead of its actual name. Each scope function uses one of two ways to access the context object: as a lambda receiver (this) or as a lambda argument (it). 

  • runwith, and apply refer to the context object as a lambda receiver – by keyword this.
  • let and also have the context object as a lambda argument. If the argument name is not specified, the object is accessed by the implicit default name it

Result

Scope functions differ by the result they return:

  • apply and also return the context object.
  • letrun, and with return the lambda result.

.apply

Let’s take a look at the apply function first. The apply function can be seen as an object configuration function. It allows you to call a series of functions on a receiver to configure it for use. After a lambda provided to apply is executed, apply returns the configured receiver (context object).

So, in the example below, if you have a screen in the WooCommerce mobile app where you have different rows, each one takes you to a different screen. But you would like to add an additional feature that will be available only if the store is located in a country that supports that specific feature.

When you define the function to create the screen UI you can use apply to add the row to access the new feature only if the store is located in a supported country and then you have as a result the context object (the list of rows):

private fun createSettingsOption(): List<ListItem> = mutableListOf(
       
        ListItem(
            icon = R.drawable.ic_one,
            label = UiStringRes(R.string.itemOne),
            onClick = ::onItemOneClicked
        ),
        ListItem(
            icon = R.drawable.ic_two,
            label = UiStringRes(R.string.itemTwo),
            onClick = ::onItemTwoClicked
        ),
    ).apply {
        if (storeCountry is supportedCountry) {
            add(
                ListItem(
                    icon = R.drawable.ic_three,
                    label = UiStringRes(R.string.itemThree),
                    onClick = { onItemThreeClicked(storeCountry) }
                )
            )
        }
    }

.let

let can be used to invoke one or more functions on the results of call chains. Here, the context object is available as an argument (it). The return value is the lambda result. It scopes a variable to the lambda provided and passes the receiver as an argument.

Imagine this scenario:

Four hobbits enter the Prancing Pony tavern. A suspicious fellow sits in the back of the main room. He wears a hoody which is concealing his face. When he smokes his pipe, the ember lights up his face, partially, but it’s not enough to find out who he is (none of this is relevant to the example by the way).

Which of the four hobbits will notice the stranger first?

We can find out like this:

fun main() {

    println(whisper)

}

val hobbits: List<String> = listOf("Frodo", "Sam", "Merry", "Pippin")

val whisper = hobbits.random().let {
    "$it notices the stranger sitting in the back and whisper to the others: We should get out of here"
}

This is a silly and simple example, but even tho it’s pretty simple, we have at least one less element in the code using .let as opposed to what we would need without the .let scope function.

Without it, I would need another variable to store the hobbits.random( ) option from the list and then use that variable to come up with the whisper string.

let can be used to invoke one or more functions on the results of call chains. For example, the following code prints the results of two operations on a collection:

val names: List<String> = listOf("Sam", "Frodo", "Merry", "Pippin", "Aragorn", "Gandalf")
val result = names.map { it.length }.filter { it > 5 }
println(result)

With let, you can rewrite it:

val names: List<String> = listOf("Sam", "Frodo", "Merry", "Pippin", "Aragorn", "Gandalf")
     names.map { it.length }.filter { it > 4 }.let {
         println(it)
         // and more function calls if needed
     }

.with

Unlike the scope functions above, with requires its argument to be accepted as the first parameter rather than calling the scope function on a receiver type. The context object is passed as an argument, but inside the lambda, it’s available as a receiver (this). The return value is the lambda result. It can be read as “with this object, do the following.

Yeah… So, for example:

val hobbits = mutableListOf("Sam", "Frodo", "Merry", "Pippin")
with(hobbits) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

And the output is:

'with' is called with argument [Sam, Frodo, Merry, Pippin]
It contains 4 elements

Another use case for with is introducing a helper object whose properties or functions will be used for calculating a value.

val numbers = mutableListOf("one", "two", "three")

val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

.run

run provides the same relative scoping behavior as apply, and it returns the result of the lambda expression, instead of the receiver itself, like let.

Let’s say you would like to keep track of the music being played at a bbq party.

val bbqPlaylist = mutableListOf("Wherever I may roam", "Fear of the Dark", "Back in Black")
    val nowPlaying: String = bbqPlaylist.run {
        shuffle()
        "${first()} is currently playing"
    }
    println(nowPlaying)

The shuffle() function is implicitly performed on the receiver – the bbqPlaylist List of strings – and it returns the lambda result which is the message saying which song is playing at the moment. The song is selected by getting the first ${first()} element of the shuffled list.

.also

Last but not least, the .also function, which ALSO works very similarly to the .let function, passes the receiver you call it on as an argument to the lambda you provide, but the major difference is that .also returns the receiver instead of the result of the lambda.

also is good for performing some actions that take the context object as an argument. Use also for actions that need a reference to the object rather than its properties and functions.

val currentFellowshipOfTheRing = mutableListOf("Frodo", "Merry", "Pippin", "Sam")
    currentFellowshipOfTheRing
        .also { println("Aragorn say to $it: If by my life or death I can protect you, I will. You have my sword") }
        .add("Aragorn")
        .also { println(currentFellowshipOfTheRing) }

The output is:

Aragorn say to [Frodo, Merry, Pippin, Sam]: If by my life or death I can protect you, I will. You have my sword
[Frodo, Merry, Pippin, Sam, Aragorn]
To summarize:

Here’s a simple table to help you decide which scope function will work better in your situation:

FunctionObject referenceReturn valueIs extension function
letitLambda resultYes
runthisLambda resultYes
runLambda resultNo: called without the context object
withthisLambda resultNo: takes the context object as an argument.
applythisContext objectYes
alsoitContext objectYes
source kotlinlang.org

In a nutshell, scope functions are best used when you want to temporarily create a new scope or change the scope that your program is running in. Any time you would use a temporary variable might be a good time to consider using a scope function.

One thought on “Scope Functions

Comments are closed.

%d bloggers like this: