🚀 · with(), run() vs. with()

6 min read · Updated on by

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)
}

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer