In this tutorial by Samuel Urbanowicz, a Kotlin expert, you’ll explore four useful extension functions from the Kotlin standard library—let, also, apply, and run. They work great together with lambda expressions and help to write clean and safe code.

Working with let, also, and apply

Assume you can fetch data using the following function :

fun getPlayers(): List<Player&gt;?

Here, the Player class is defined as follows:

data class Player(val name: String, val bestScore: Int)

How would you perform the following sequence of operations to the getPlayers() function result?

  1. Print the original set of players in the list to the console
  2. Sort the collection of the Player objects in descending order
  3. Transform the collection of Player objects into a list of strings obtained from the Player.name property
  4. Limit the collection to the first element and print it to the console

In order to accomplish the task, you first need to get familiar with the characteristics of the letalso, and apply functions. They are provided in the standard library as extension functions for a generic type. Here are the headers of the letalso, and apply functions:

public inline fun <T, R&gt; T.let(block: (T) -&gt; R): R

public inline fun <T&gt; T.also(block: (T) -&gt; Unit): T

public inline fun <T&gt; T.apply(block: T.() -&gt; Unit): T

They look similar; however, there are some subtle differences in the return types and in parameters. The following table compares the three functions:

FunctionReturn typeArgument in block argumentBlock argument definition
LetR (from block body)Explicit it(T) -> R
AlsoT (this)Explicit it(T) -> Unit
ApplyT (this)Implicit thisT.() -> Unit

How to do it…

  1. Use the let function together with the safe operator to assure null safety:
    getPlayers()?.let {}
  2. Inside the let function’s lambda parameter block, use the also() function to print the original set of players in the list to the console:
    getPlayers()?.let {
        it.also {
    println("${it.size} players records fetched")
    println(it)
    }
    }
  3. Use the let() function to perform sorting and mapping transformations:
    getPlayers()?.let {
        it.also {
    println("${it.size} players records fetched")
    println(it)
    }.let {
            it.sortedByDescending { it.bestScore }
        }
  4. Limit the collection of players to a single Player instance with the highest score using the let() function:
    getPlayers()?.let {
        it.also {
    println("${it.size} players records fetched")
    println(it)
    }.let {
            it.sortedByDescending { it.bestScore }
        }.let {
            it.first()
    }
  5. Print the name of the best player to the console:
    getPlayers()?.let {
        it.also {
    println("${it.size} players records fetched")
    println(it)
    }.let {
            it.sortedByDescending { it.bestScore }
        }.let {
            it.first()
    }.apply {
    val name = this.name
    print("Best Player: $name")
    }
    }

How it works…

For the sake of testing the implementation, you can assume that the getPlayers() function returns the following results:

fun getPlayers(): List<Player&gt;? = listOf(
        Player("Stefan Madej", 109),
        Player("Adam Ondra", 323),
        Player("Chris Charma", 239))

The code you have implemented will print the following output to the console:

3 players records fetched
[Player(name=Stefan Madej, bestScore=109), Player(name=Adam Ondra, bestScore=323), Player(name=Chris Charma, bestScore=239)]
Best Player: Adam Ondra

Note that, in the case of the apply() function, you can omit the this keyword while accessing class properties and functions inside the function lambda block:

apply {
print("Best Player: $name")
}

It was used in the above example code just for the sake of clarity.

The most useful feature of the let() function is that it can be used to assure the null safety of the given object. In the following example, inside the let scope, the players argument will always hold a not null value even if some background thread tries to modify the original value of the mutable results variable:

var result: List<Player&gt;? = getPlayers()
result?.let { players: List<Player&gt; -&gt;
    ...
}

Initializing objects using the run scoping function

In this recipe, you’ll explore another useful extension function provided by the standard library: run(). You’ll use it to create and set up an instance of the java.util.Calendar class.

But before that, you need to understand the characteristics of the run() function defined in the standard library. The following is its function header:

public inline fun <T, R&gt; T.run(block: T.() -&gt; R): R

It is declared as an extension function for a generic type. The run function provides an implicit this parameter inside the block argument and returns the result of the block execution.

How to do it…

  1. Declare an instance of the Calendar.Builder class and apply the run() function to it:
    val calendar = Calendar.Builder().run {
    build()
    }
  2. Add the desired properties to the builder:
    val calendar = Calendar.Builder().run {
    setCalendarType("iso8601")
        setDate(2018, 1, 18)
        setTimeZone(TimeZone.getTimeZone("GMT-8:00"))
        build()
    }
  3. Print the date from the calendar to the console:
    val calendar = Calendar.Builder().run {
    setCalendarType("iso8601")
        setDate(2018, 1, 18)
        setTimeZone(TimeZone.getTimeZone("GMT-8:00"))
        build()
    }
    print(calendar.time)

How it works…

The run function is applied to the Calendar.Builder instance. Inside the lambda passed to the run function, you can access the Calendar.Builder properties and methods via the this modifier. In other words, inside the run function block, you can access the scope of the Calendar.Builder instance. In the above code, you’ve omitted to invoke Builder methods with the this keyword. You can call them directly because the run function allows accessing the Builder instance inside its scope via an implicit this modifier.

There’s more…

You can also use the run() function together with the safe ? operator to provide null safety of the object referenced by the this keyword inside the run() function scope. You can see it in action in the following code snippet to configure the Android WebView class:

webview.settings?.run {
    this.javaScriptEnabled = true
    this.domStorageEnabled = false
}

In the preceding code snippet, you are ensuring that the settings property is not null inside the run function scope and you can access it with the this keyword.

The Kotlin standard library offers another similar extension function, called apply(), which is useful for the initialization of objects. The main difference is that it returns an original instance of the object it was called on. You can explore the apply() function and more in the book, Kotlin Standard Library Cookbook, a hands-on guide packed with a collection of executable recipes that address versatile programming pain points using the Kotlin standard library. So, if you are a software developer familiar with Kotlin’s basics and want to discover more advanced features and concepts, Kotlin Standard Library Cookbook is a must-read!