//

Let’s also apply run with Kotlin scope functions

8.7.2019 | 5 minutes of reading time

Scope functions

In Kotlin, scope functions allow you to execute a function, i.e. a block of code, in the context of an object. The object is then accessible in that temporary scope without using the name. Although whatever you do with scope functions can be done without, they enable you to structure your code differently. Using them can increase readability and make your code more concise.

The Kotlin standard library offers four different types of scope functions which can be categorized by the way they refer to the context object and the value they return. A scope function either refers to the context object as a function argument or a function receiver. The return value of a scope function is either the function result or the context object.

The available functions are letalsoapplyrun, and with. The following table summarizes the characteristics of each function based on the way the context object can be accessed and the return type as described above:

Context Object As Function ArgumentContext Object As Function Receiver
Returns: Function Resultletrunwith
Returns: Context Objectalsoapply

The difference between run and with lies only in the way they are called. While all other scope functions are implemented as extension functions, with is a regular function.

Now that I’ve mentioned concepts such as function receivers and extension functions it makes sense to briefly explain them before we move on into the detailed descriptions of the scope functions. If you are already familiar with function receivers and extension functions in Kotlin you can skip the next section.

Function arguments, extension functions, receivers

Kotlin allows for treating functions as values. This means you can pass functions as arguments to other functions. Using the :: operator you can convert a method to a function value. To increase readability, the last function argument can be placed outside of the argument list.

The following example illustrates how to do that by defining a higher order function combine, which takes a function argument f. We’re invoking it with the plus method from the Int class and with an anonymous function literal both within the and outside of the argument list:

1// Apply function argument f to integers a and b
2fun combine(a: Int, b: Int, f: (Int, Int) -> Int): Int = f(a, b)
3
4// Using the plus method as a function value
5combine(1, 2, Int::plus)
6
7// Passing a function literal
8combine(1, 2, { a, b ->
9    val x = a + b
10    x + 100
11})
12
13// Passing it outside of the argument list
14combine(1, 2) { a, b ->
15    val x = a + b
16    x + 100
17}
18

Extension functions are a way to extend existing classes or interfaces you do not necessarily have under your control. Defining an extension function on a class lets you call this method on instances of that class as if it was part of the original class definition.

The following example defines an extension function on Int to return the absolute value:

1fun Int.abs() = if (this < 0) -this else this
2
3(-5).abs() // 5
4

Function literals with receiver are similar to extension functions as the receiver object is accessible within the function through this. The following code snippet defines the extension function from before but this time as a function literal with receiver:

1val abs: Int.() -> Int = { if (this < 0) -this else this }
2
3(-5).abs() // 5
4

A common use case for function literals with receivers are type-safe builders . Now that we have covered the basics let’s look at the five scope functions individually.

Let, also, apply, run, with

Let

The let scope function makes the context object available as a function argument and returns the function result. A typical use case is applying null-safe transformations to values.

1val x: Int? = null
2
3// null-safe transformation without let
4val y1 = if (x != null) x + 1 else null
5val y2 = if (y1 != null) y1 / 2 else null
6
7// null-safe transformation with let
8val z1 = x?.let { it + 1 }
9val z2 = z1?.let { it / 2 }
10

Also

The apply scope function makes the context object available as a function argument and returns the context object. This can be used when you are computing a return value inside a function and then want to apply some side effect to it before you return it.

1// assign, print, return
2fun computeNormal(): String {
3    val result = "result"
4    println(result)
5    return result
6}
7
8// return and also print
9fun computeAlso(): String =
10    "result".also(::println)
11

Apply

The apply scope function makes the context object available as a receiver and returns the context object. This makes it very useful for “ad-hoc builders” of mutable objects, such as Java Beans.

1// Java Bean representing a person
2public class PersonBean {
3    private String firstName;
4    private String lastName;
5    public void setFirstName(String firstName) {
6        this.firstName = firstName;
7    }
8    public String getFirstName() {
9        return firstName;
10    }
11    public void setLastName(String lastName) {
12        this.lastName = lastName;
13    }
14    public String getLastName() {
15        return lastName;
16    }
17}
18
1// Initialization the traditional way
2val p1 = PersonBean()
3p1.firstName = "Frank"
4p1.lastName = "Rosner"
5
6// Initialization using apply
7val p2 = PersonBean().apply {
8    firstName = "Frank"
9    lastName = "Rosner"
10}
11

Run and with

The run scope function makes the context object available as a receiver and returns the function result. It can be used with or without a receiver. When using it without a receiver you can compute an expression using locally scoped variables. By using a receiver, run can be called on any object, e.g. a connection object.

1// compute result as block result
2val result = run {
3    val x = 5
4    val y = x + 3
5    y - 4
6}
7
8// compute result with receiver
9val result2 = "text".run {
10    val tail = substring(1)
11    tail.toUpperCase()
12}
13

The with function works exactly as run but is implemented as a regular function and not an extension function.

1val result3 = with("text") {
2    val tail = substring(1)
3    tail.toUpperCase()
4}
5

Summary

In this post we learned about the scope functions letalsoapplyrun, and with. They differ in the way they refer to the context object and the value they return. Combined with the concepts of function arguments, extension functions and receivers, scope functions are a useful tool to produce more readable code.

What do you think about scope functions? Have you ever used them in one of your projects? Can you remember which one to use when? Let me know your thoughts in the comments!

References

share post

Likes

0

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.