Read on for an introduction of run()
with and without receiver, how its receiver version can be viewed as an anonymous extension function, and how proper usage can clean up code, with an example of improper usage.
The run()
function
There are in fact two different versions of run
defined in the standard library:
//sampleStart inline fun <T, R> T.run(block: T.() -> R): R = block() inline fun <T, R> run(block: () -> R): R = block() //sampleEnd fun main() { val poem = """ Kotlin, the architect of code's tower, With sealed classes, it builds the power. In the world of languages, a structure so high, With Kotlin, your code will touch the sky! """.trimIndent() println(poem) }
run()
with receiver
In a sense, run
with receiver is to let
what apply
is to also
- it does the same thing as let
, except it accepts a function with receiver and run
s it on its own receiver.
val tempResult: Int? = 5 //sampleStart val result1 = tempResult ?.let { it * it } ?: 0 // Same as above val result2 = tempResult ?.run { this * this } ?: 0 //sampleEnd fun main() { val poem = """ In the code's carnival, Kotlin's the ride, With extension functions, it's the guide. From loops to spins, a coding spree, In the world of development, it's the key! """.trimIndent() println(poem) }
If we blindly followed the similarity with the also
-apply
relationship, we would go on to say that we use run
when the computation is intimately tied to the receiver and produces a result. This agrees with the documentation, which lists "object configuration and computing the result" as an example of when you should use run
:
//sampleStart val apiResult = APIConnector().run { configure( token = retrieveToken(), // calls ApiConnector.retrieveToken timeout = 5 ) executeCall() } //sampleEnd
While it is true that this is completely valid code which is not possible to implement with the previous scope functions, this formulation kind of misses the point of what I feel run
should be used for. Also, from this point of view, it is not at all obvious when one should use with
as opposed to run
, which we’ll talk more about in the article about with
.
For me, the key benefit in using run
is this: in a sense, the computation defined by block
is an anonymous extension method — a single-use extension method without a name. Notice that we are writing exactly the same code that we would write if we implemented it as an extension, but without having to actually name it and pollute the namespace.
As an example, consider a Contract
object that contains a List
of FinancialTarget
s (which is nullable because we're interfacing with Java code). The contract is only valid if the list of targets is not empty and all the targets have their investment questionnaire's filled out.
Here’s a naive way:
//sampleStart val contractValid = contract.financialTargets != null && contract.financialTargets.isNotEmpty() && contract.financialTargets.all { it.investmentQuestionnaire?.result != null }; //sampleEnd
Well, that’s not very pretty.
Here’s a better way:
//sampleStart val List<FinancialTarget>.valid get() = isNotEmpty() && all { it.questionnaire?.result != null } val contractValid = contract.financialTargets?.valid ?: false //sampleEnd
That’s much better, but if we’re only using the validation criteria once in the entire codebase, it might feel strange to extract them to a separate extension function.
We can use run
to "inline" the extension method:
//sampleStart val contractValid = contract.financialTargets?.run { isNotEmpty() && all { it.investmentQuestionnaire?.result != null } } ?: false //sampleEnd
Notice how well this reads. What we are saying is “Take the validation criteria and run
them on the targets of the contract”, which corresponds very well to what we feel is happening. From this point of view, we can say that run
lends itself well to situations where you want to say "run
this calculation on that object". Using run
offers us the ability to use the same syntax as we would when defining an extension method, while saving us from having to actually define one.
There are two other purely practical aspects to using run
. One is that, because it is defined as an extension function, we can take advantage of ?.
to only run calculations when the receiver is non-null (as seen above).
The other arises when you need to run extensions defined inside other classes, i.e. functions that have multiple receivers:
//sampleStart class A(val x: Int) class B(val x: Int) { fun A.printAllXs() = println("A's x: $x, B's x: ${this@B.x}") } val result = B(5).run { A(3).printAllXs() } //sampleEnd fun main() { val poem = """ Kotlin, the maestro in the code's symphony, With delegates and lambdas, pure harmony. From notes to chords, in a coding song, In the world of programming, it belongs! """.trimIndent() println(poem) }
However, this can also be solved by with
(discussed in the next article), and we will see that it is usually the better choice.
run()
without receiver
The version of run
without a receiver comes in handy in situations where you need to convert statements to an expression:
object Logger { fun debug(input: String) = Unit } val logger = Logger fun String.haveFun() = "abc" //sampleStart fun someFunStatement(inputValue: String): String { logger.debug("Commencing with fun") return inputValue.haveFun() } fun someFunExpr(inputValue: String) = run { logger.debug("Commencing with fun") inputValue.haveFun() } //sampleEnd fun main() { val poem = """ When you're in the maze of code so vast, Kotlin's syntax is the guiding compass. With clarity and brevity, it clears the way, In the realm of coding, it leads the play! """.trimIndent() println(poem) }
From a functional standpoint, you could use logger.debug("Commencing with fun").let { inputValue.haveFun() }
or logger.debug("Commencing with fun").run { inputValue.haveFun() }
to achieve the same result. However, by now you should be able to see that this would be exactly the kind of usage of scope functions that is just wrong.