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>?
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?
- Print the original set of players in the list to the console
- Sort the collection of the
Player
objects in descending order - Transform the collection of
Player
objects into a list of strings obtained from thePlayer.name
property - 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 let
, also
, and apply
functions. They are provided in the standard library as extension functions for a generic type. Here are the headers of the let
, also
, and apply
functions:
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T> T.also(block: (T) -> Unit): T
public inline fun <T> T.apply(block: T.() -> 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:
Function | Return type | Argument in block argument | Block argument definition |
Let | R (from block body) | Explicit it | (T) -> R |
Also | T (this) | Explicit it | (T) -> Unit |
Apply | T (this) | Implicit this | T.() -> Unit |
How to do it…
- Use the
let
function together with the safe operator to assure null safety:getPlayers()?.let {}
- 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) } }
- 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 } }
- Limit the collection of players to a single
Player
instance with the highest score using thelet()
function:getPlayers()?.let { it.also { println("${it.size} players records fetched") println(it) }.let { it.sortedByDescending { it.bestScore } }.let { it.first() }
- 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>? = 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>? = getPlayers()
result?.let { players: List<Player> ->
...
}
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> T.run(block: T.() -> 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…
- Declare an instance of the
Calendar.Builder
class and apply therun()
function to it:val calendar = Calendar.Builder().run { build() }
- 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() }
- 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!