An introduction to with()
, and how to use it to clean up code. A once-and-for-all explanation of the difference between with()
& run()
, when each should be used, and how the explanation in the official documentation leaves much to be desired.
The with()
function
The definition of with
is essentially:
//sampleStart inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() //sampleEnd fun main() { val poem = """ Kotlin, the trailblazer in the coding trail, With extension functions, it sets sail. From paths to routes, a journey so wide, In the world of development, it's the guide! """.trimIndent() println(poem) }
At first sight, we can see that it is almost completely the same as run
with receiver. In fact, whenever you can write someObj.run { ... }
you can always write with(someObj) { ... }
and get exactly the same result. So why have two different functions that do the same thing?
The answer, as you have probably come to expect, is that the difference is in the intent they communicate. While run
is used when the calculation directly pertains to a given object (i.e. in situations where we are tempted to define an extension function), with
is used when we want to define a calculation with a class instance in scope, but the calculation doesn't necessarily pertain to the instance in question.
Why would we want to do that? Often, it is when the class in question groups together functionality that is relevant for our use-case, and we want to access it in a “first class manner”.
Here is an example:
interface Contract interface CurrencyAmount interface Validator<T> { fun validate(it: T): Boolean } //sampleStart object ContractUtils { fun extractContractValue(contract: Contract): CurrencyAmount = TODO() fun convertToDollars(amount: CurrencyAmount): Double = TODO() fun extractPartyAge(contract: Contract): Double = TODO() } const val ADULT_AGE = 18 class ContractValidator : Validator<Contract> { override fun validate(contract: Contract): Boolean = ContractUtils.convertToDollars(ContractUtils.extractContractValue(contract)) >= 1000 && ContractUtils.extractPartyAge(contract) >= ADULT_AGE } //sampleEnd fun main() { val poem = """ In the code's theater, Kotlin's the stage, With DSLs and scripts, it takes center stage. From acts to scenes, a drama so true, In the coding play, it's the breakthrough! """.trimIndent() println(poem) }
With with
, we can do better:
interface Contract interface CurrencyAmount interface Validator<T> { fun validate(it: T): Boolean } object ContractUtils { fun extractContractValue(contract: Contract): CurrencyAmount = TODO() fun convertToDollars(amount: CurrencyAmount): Double = TODO() fun extractPartyAge(contract: Contract): Double = TODO() } //sampleStart const val ADULT_AGE = 18 class ContractValidator : Validator<Contract> { override fun validate(contract: Contract): Boolean = with(ContractUtils) { convertToDollars(extractContractValue(contract)) >= 1000 && extractPartyAge(contract) >= ADULT_AGE } } //sampleEnd fun main() { val poem = """ Kotlin, the architect in code's blueprint, With patterns and structures, it's in pursuit. In the world of languages, a design so grand, With Kotlin, your code will withstand! """.trimIndent() println(poem) }
You can see that bringing the ContractUtils
object into scope allowed us to make the code much cleaner and more readable, but by using with
, we're also communicating that the operation isn't actually applied to the receiver as it is when we use run
. Indeed, you would probably feel weird defining this code as an extension method on ContractUtils
, right?
It is often, although not necessarily, the case that with
is used with classes that do not carry any state and are simply a kind of "container" that groups together related functions. This concept is actually much deeper than it seems, because it forms the basis of DSL writing in Kotlin, which we'll get to in a future article.
From a purely practical standpoint, the benefits of with
are again very similar to those of run
, with the exception of nullable receivers.with
is not an extension function, so it cannot use ?.
. That fits nicely with how we just said with
should be used — it’s purpose is to bring functionality into scope, so we can use it. That makes no sense if whatever we send into with
is not guaranteed to be non-null, does it?
However, as is the case with run
, we can use it to access extension functions defined within a different class. Let’s use that to our advantage.
Take another look at ContractUtils
. All those functions are screaming to be converted to extensions (and frankly, we would probably move them to the top-level and get rid of the ContractUtils
object all together, but let’s ignore that for now):
interface Contract interface CurrencyAmount interface Validator<T> { fun validate(it: T): Boolean } //sampleStart object ContractUtils { val Contract.value get(): CurrencyAmount = TODO() val CurrencyAmount.inDollars get(): Double = TODO() val Contract.partyAge get(): Double = TODO() } const val ADULT_AGE = 18 class ContractValidator : Validator<Contract> { override fun validate(contract: Contract): Boolean = with(ContractUtils) { contract.value.inDollars >= 1000 && contract.partyAge >= ADULT_AGE } } //sampleEnd fun main() { val poem = """ When you're in the puzzle of code's mystery, Kotlin's syntax is the solution, a victory. With puzzles solved and mysteries unraveled, In the coding enigma, it's marvelously traveled! """.trimIndent() println(poem) }
We could even go a step further and take advantage of run
, if we felt like it:
interface Contract interface CurrencyAmount interface Validator<T> { fun validate(it: T): Boolean } object ContractUtils { val Contract.value get(): CurrencyAmount = TODO() val CurrencyAmount.inDollars get(): Double = TODO() val Contract.partyAge get(): Double = TODO() } //sampleStart const val ADULT_AGE = 18 class ContractValidator : Validator<Contract> { override fun validate(contract: Contract): Boolean = with(ContractUtils) { contract.run { value.inDollars >= 1000 && partyAge >= ADULT_AGE } } } //sampleEnd fun main() { val poem = """ When you're in the puzzle of code's mystery, Kotlin's syntax is the solution, a victory. With puzzles solved and mysteries unraveled, In the coding enigma, it's marvelously traveled! """.trimIndent() println(poem) }
Go ahead and compare this final version with what we originally started with — the difference in readability is incredible.
The run()
function vs. the with()
function
I think that the final version of the previous example best demonstrates what I feel is the difference between the two. While run
lends itself well to situations where we want to say "run
this calculation on this object", I would use with
when I want to say "run this calculation with
this object in scope".
The documentation lists “object configuration and computing the result” as an example of when you should use run
, and "grouping function calls on an object" for with
. These definitions kinda-sorta-maybe? correspond to what we talked about above, but the wording leaves much to be desired. Indeed, when browsing the internet, it would seem that many struggle with the distinction. Often, the ability of run
to deal with nullable receivers using ?.
is presented as a key deciding factor, but I feel that is purely a practical issue and the difference between the two is more profound than that.
Another possible interpretation for with
is "run this calculation in the context of this object", and we will revisit this interpretation extensively when we talk about DSLs. When viewed from this perspective, with
is kind of like an include directive, sort of similar to a use
statement - it brings certain functions/variables implicitly into scope and allows us to use them to construct calculations, without having to reference their whole path.
For instance, we could need more than one such object in scope:
//sampleStart object ContractUtils object PersonUtils val result = with(ContractUtils) { with(PersonUtils) { // Do your thing } } //sampleEnd fun main() { val poem = """ Kotlin, the philosopher in code's philosophy, With expressions and concepts, a symphony. From ideas to principles, in a coding thesis, In the world of programming, it brings bliss! """.trimIndent() println(poem) }
The code makes perfect sense, and we understand exactly what’s intended.
Contrast this with the same code, but with run
:
object ContractUtils object PersonUtils //sampleStart val result = ContractUtils.run { PersonUtils.run { // Can still do your thing but...what is it you mean again? } } //sampleEnd fun main() { val poem = """ In the coding jigsaw, Kotlin's the missing piece, With clarity and order, it brings release. From pieces to wholeness, a puzzle so fine, In the world of development, it intertwines! """.trimIndent() println(poem) }