Intermediate: Scope functions
In this chapter, you'll build on your understanding of extension functions to learn how to use scope functions to write more idiomatic code.
Scope functions
In programming, a scope is the area in which your variable or object is recognized. The most commonly referred to scopes are the global scope and the local scope:
Global scope – a variable or object that is accessible from anywhere in the program.
Local scope – a variable or object that is only accessible within the block or function where it is defined.
In Kotlin, there are also scope functions that allow you to create a temporary scope around an object and execute some code.
Scope functions make your code more concise because you don't have to refer to the name of your object within the temporary scope. Depending on the scope function, you can access the object either by referencing it via the keyword this or using it as an argument via the keyword it.
Kotlin has five scope functions in total: let, apply, run, also, and with.
Each scope function takes a lambda expression and returns either the object or the result of the lambda expression. In this tour, we explain each scope function and how to use it.
Let
Use the let scope function when you want to perform null checks in your code and later perform further actions with the returned object.
Consider the example:
The example has two functions:
sendNotification(), which has a function parameterrecipientAddressand returns a string.getNextAddress(), which has no function parameters and returns a string.
The example creates a variable address that has a nullable String type. But this becomes a problem when you call the sendNotification() function because this function doesn't expect that address could be a null value. The compiler reports an error as a result:
From the beginner tour, you already know that you can perform a null check with an if condition or use the Elvis operator ?:. But what if you want to use the returned object later in your code? You could achieve this with an if condition and an else branch:
However, a more concise approach is to use the let scope function:
The example:
Creates variables called
addressandconfirm.Uses a safe call for the
letscope function on theaddressvariable.Creates a temporary scope within the
letscope function.Passes the
sendNotification()function as a lambda expression into theletscope function.Refers to the
addressvariable viait, using the temporary scope.Assigns the result to the
confirmvariable.
With this approach, your code can handle the address variable potentially being a null value, and you can use the confirm variable later in your code.
Apply
Use the apply scope function to initialize objects, like a class instance, at the time of creation rather than later on in your code. This approach makes your code easier to read and manage.
Consider the example:
The example has a Client class that contains one property called token and three member functions: connect(), authenticate(), and getData().
The example creates client as an instance of the Client class before initializing its token property and calling its member functions in the main() function.
Although this example is compact, in the real world, it can be a while before you can configure and use the class instance (and its member functions) after you've created it. However, if you use the apply scope function you can create, configure and use member functions on your class instance all in the same place in your code:
The example:
Creates
clientas an instance of theClientclass.Uses the
applyscope function on theclientinstance.Creates a temporary scope within the
applyscope function so that you don't have to explicitly refer to theclientinstance when accessing its properties or functions.Passes a lambda expression to the
applyscope function that updates thetokenproperty and calls theconnect()andauthenticate()functions.Calls the
getData()member function on theclientinstance in themain()function.
As you can see, this strategy is convenient when you are working with large pieces of code.
Run
Similar to apply, you can use the run scope function to initialize an object, but it's better to use run to initialize an object at a specific moment in your code and immediately compute a result.
Let's continue the previous example for the apply function, but this time, you want the connect() and authenticate() functions to be grouped so that they are called on every request.
For example:
The example:
Creates
clientas an instance of theClientclass.Uses the
applyscope function on theclientinstance.Creates a temporary scope within the
applyscope function so that you don't have to explicitly refer to theclientinstance when accessing its properties or functions.Passes a lambda expression to the
applyscope function that updates thetokenproperty.
The main() function:
Creates a
resultvariable with typeString.Uses the
runscope function on theclientinstance.Creates a temporary scope within the
runscope function so that you don't have to explicitly refer to theclientinstance when accessing its properties or functions.Passes a lambda expression to the
runscope function that calls theconnect(),authenticate(), andgetData()functions.Assigns the result to the
resultvariable.
Now you can use the returned result further in your code.
Also
Use the also scope function to complete an additional action with an object and then return the object to continue using it in your code, like writing a log.
Consider the example:
The example:
Creates the
medalsvariable that contains a list of strings.Creates the
reversedLongUpperCaseMedalsvariable that has theList<String>type.Uses the
.map()extension function on themedalsvariable.Passes a lambda expression to the
.map()function that refers tomedalsvia theitkeyword and calls the.uppercase()extension function on it.Uses the
.filter()extension function on themedalsvariable.Passes a lambda expression as a predicate to the
.filter()function that refers tomedalsvia theitkeyword and checks if the item in the list has more than 4 characters.Uses the
.reversed()extension function on themedalsvariable.Assigns the result to the
reversedLongUpperCaseMedalsvariable.Prints the list contained in the
reversedLongUpperCaseMedalsvariable.
It would be useful to add some logging in between the function calls to see what is happening to the medals variable. The also function helps with that:
Now the example:
Uses the
alsoscope function on themedalsvariable.Creates a temporary scope within the
alsoscope function so that you don't have to explicitly refer to themedalsvariable when using it as a function parameter.Passes a lambda expression to the
alsoscope function that calls theprintln()function using themedalsvariable as a function parameter via theitkeyword.
Since the also function returns the object, it is useful for not only logging but debugging, chaining multiple operations, and performing other side-effect operations that don't affect the main flow of your code.
With
Unlike the other scope functions, with is not an extension function, so the syntax is different. You pass the receiver object to with as an argument.
Use the with scope function when you want to call multiple functions on an object.
Consider this example:
The example creates a Canvas class that has three member functions: rect(), circ(), and text(). Each of these member functions prints a statement constructed from the function parameters that you provide.
The example creates mainMonitorPrimaryBufferBackedCanvas as an instance of the Canvas class before calling a sequence of member functions on the instance with different function parameters.
You can see that this code is hard to read. If you use the with function, the code is streamlined:
This example:
Uses the
withscope function with themainMonitorSecondaryBufferBackedCanvasinstance as the receiver.Creates a temporary scope within the
withscope function so that you don't have to explicitly refer to themainMonitorSecondaryBufferBackedCanvasinstance when calling its member functions.Passes a lambda expression to the
withscope function that calls a sequence of member functions with different function parameters.
Now that this code is much easier to read, you are less likely to make mistakes.
Use case overview
This section has covered the different scope functions available in Kotlin and their main use cases for making your code more idiomatic. You can use this table as a quick reference. It's important to note that you don't need a complete understanding of how these functions work in order to use them in your code.
Function | Access to | Return value | Use case |
|---|---|---|---|
|
| Lambda result | Perform null checks in your code and later perform further actions with the returned object. |
|
|
| Initialize objects at the time of creation. |
|
| Lambda result | Initialize objects at the time of creation AND compute a result. |
|
|
| Complete additional actions before returning the object. |
|
| Lambda result | Call multiple functions on an object. |
For more information about scope functions, see Scope functions.
Practice
Exercise 1
Rewrite the .getPriceInEuros() function as a single-expression function that uses safe call operators ?. and the let scope function.
- Hint
Use safe call operators
?.to safely access thepriceInDollarsproperty from thegetProductInfo()function. Then, use theletscope function to convert the value ofpriceInDollarsinto euros.
Exercise 2
You have an updateEmail() function that updates the email address of a user. Use the apply scope function to update the email address and then the also scope function to print a log message: Updating email for user with ID: ${it.id}.