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 parameterrecipientAddress
and 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 a variable called
confirm
.Uses a safe call for the
let
scope function on theaddress
variable.Creates a temporary scope within the
let
scope function.Passes the
sendNotification()
function as a lambda expression into thelet
scope function.Refers to the
address
variable viait
, using the temporary scope.Assigns the result to the
confirm
variable.
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
client
as an instance of theClient
class.Uses the
apply
scope function on theclient
instance.Creates a temporary scope within the
apply
scope function so that you don't have to explicitly refer to theclient
instance when accessing its properties or functions.Passes a lambda expression to the
apply
scope function that updates thetoken
property and calls theconnect()
andauthenticate()
functions.Calls the
getData()
member function on theclient
instance 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
client
as an instance of theClient
class.Uses the
apply
scope function on theclient
instance.Creates a temporary scope within the
apply
scope function so that you don't have to explicitly refer to theclient
instance when accessing its properties or functions.Passes a lambda expression to the
apply
scope function that updates thetoken
property.
The main()
function:
Creates a
result
variable with typeString
.Uses the
run
scope function on theclient
instance.Creates a temporary scope within the
run
scope function so that you don't have to explicitly refer to theclient
instance when accessing its properties or functions.Passes a lambda expression to the
run
scope function that calls theconnect()
,authenticate()
, andgetData()
functions.Assigns the result to the
result
variable.
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
medals
variable that contains a list of strings.Creates the
reversedLongUpperCaseMedals
variable that has theList<String>
type.Uses the
.map()
extension function on themedals
variable.Passes a lambda expression to the
.map()
function that refers tomedals
via theit
keyword and calls the.uppercase()
extension function on it.Uses the
.filter()
extension function on themedals
variable.Passes a lambda expression as a predicate to the
.filter()
function that refers tomedals
via theit
keyword and checks if the length of the list contained in themedals
variable is longer than 4 items.Uses the
.reversed()
extension function on themedals
variable.Assigns the result to the
reversedLongUpperCaseMedals
variable.Prints the list contained in the
reversedLongUpperCaseMedals
variable.
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
also
scope function on themedals
variable.Creates a temporary scope within the
also
scope function so that you don't have to explicitly refer to themedals
variable when using it as a function parameter.Passes a lambda expression to the
also
scope function that calls theprintln()
function using themedals
variable as a function parameter via theit
keyword.
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
with
scope function with themainMonitorSecondaryBufferBackedCanvas
instance as the receiver object.Creates a temporary scope within the
with
scope function so that you don't have to explicitly refer to themainMonitorSecondaryBufferBackedCanvas
instance when calling its member functions.Passes a lambda expression to the
with
scope 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 thepriceInDollars
property from thegetProductInfo()
function. Then, use thelet
scope function to convert the value ofpriceInDollars
into 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}
.