Scope Functions in Kotlin
What are Scope functions?
Scope functions are functions in the Kotlin standard library whose purpose is to execute a block of code within the context of an object. It provides a temporary scope when you call the function on the object with a lambda expression. In this scope, the function can access the object without its name. There are five scope functions
- let
- run
- with
- apply
- also
The basic functionality of all the scope functions is the same i.e. to execute a block of code on an object.
Scope functions don’t introduce any new technical capabilities, but they can make your code more concise and readable.
Distinction between Scope Functions
Scope functions are very similar in nature, making it important to understand the differences between them. Understanding the differences between them helps us pick the right extension. The main differences between these functions are
- How they refer to the context object.
- The return value of the function.
How do they refer to the context object?
Inside the lambda of a scope function, the context object could be referenced in 2 ways
- as a lambda receiver (
this
) - lambda argument (
it
)
this
run
, with
, and apply
reference the context object as a lambda receiver - by keyword this
. Inside the lambda, the object is available like in a class function.
class Something {
fun doSomething() {
println("doing something")
}
}
void main() {
val somethingObject = Something()
with(somethingObject) {
doSomething() // this is equivalent to this.doSomething()
}
}
In most cases this
can be omitted when accessing the members of the receiver object, making the code shorter and more readable. On the flip side, omitting this
can make it hard to differentiate between receiver members and external objects or functions. When having the context object as a receiver(this
) It is recommended that the lambdas operate mainly on the object’s member function(s) or assign values to the properties.
it
let
and also
reference the context object as a lambda argument. If the argument name is not specified the object can be accessed with the default name it
.
class Something {
var value: String? = null
}
object SomethingFactory {
fun getSomething(): Something? {
// return null or instance of something
}
}
void main() {
val somethingObject = SomethingFactory.getSomething()
somethingObject?.let {
it.value = "some value" // using default argument name.
}
}
void main() {
val somethingObject = SomethingFactory.getSomething()
somethingObject?.let { obj ->
obj.value = "some value" // using named argument
}
}
The Return value
The scope functions differ by the values they return
apply
andalso
return the context object, i.e. the caller object. It can be used for chaining function calls.let
,run
,with
returns the value of the lambda. It can be used for variable assignment or to return a value of a function.
The below table summarises the distinction between the scope functions
How to pick the right function for your use case?
Technically, scope functions can be used interchangeably for many use cases, without much of a side effect. For example the below snippets yield the same result, all three functions return an Something
object with the same configuration.
// using "with" to set the properties
public fun getSomeObject(): Something {
val someObject = Something()
with(someObject) {
value = "Some Value"
type = 1
}
return someObject
}
// using "apply" to set the properties
public fun getSomeObject(): Something {
return Something().apply {
value = "Some Value"
type = 1
}
}
// using "let" to set the properties
public fun getSomeObject(): Something {
val someObject = Something()
something.let {
it.value = "Some Value"
it.type = 1
}
return someObject
}
Below are my recommendation on when to use which scope function
let
let is often used for executing a code block only with non-null values. To perform actions on a non-null object, use the safe call operator ?. on it and call let with the actions in its lambda. Example,
class Something {
var value: String? = null
}
object SomethingFactory {
fun getSomething(): Something? {
// return null or instance of something
}
}
void main() {
val somethingObject = SomethingFactory.getSomething()
somethingObject?.let {
it.value = "some value"
}
}
with
It is recommended to use with for calling functions on the context object without providing the lambda result. In the code, with can be read as “with this object, do the following.” Example,
public fun getSomeObject(): Something {
val someObject = Something()
with(someObject) {
println("Value $value")
println("type $type")
}
return someObject
}
run
run is useful when the lambda contains both the object initialization and the computation of the return value. Example,
public fun getSomeObject(): String {
val someObject = Something()
val result = someObject.run {
value = "Some Value"
type = 1
"$value of type $type "
}
return result
}
apply
It is recommended to use apply for code blocks that don’t return a value and mainly operate on the members of the receiver object. The common case for using apply is the object configuration. Such calls can be read as “apply the following assignments to the object.” Example,
public fun getSomeObject(): Something {
return Something().apply {
value = "Some Value"
type = 1
}
}
also
It is recommended to use also in cases where the actions are taken on the object rather than the fields of the object. Such calls can be read as “and also do the following with the object.” Example,
fun main() {
val something = Something().also {
println("Initial State: $it")
}
something.value = "Some Value"
}