Why is "with(foo) { bar() }" preferred over "foo.bar()"?


#1

Hey, I’ve racked up about 15 minutes of Kotlin experience, and am wondering about with().

In this Android developer page (which is just what I happen to be using in experimenting with Kotlin), the “Show the notification” section about a third of the way down the page has this example:

with(NotificationManagerCompat.from(this)) {
    notify(notificationId, mBuilder.build())
}

First, that is roughly equivalent to this, right? (They seem to do the same thing.)

NotificationManagerCompat.from(this).notify(notificationId, mBuilder.build())

If so, is the first approach the generally preferred way to do it in Kotlin (and if so, why), or is the author of that article a weirdo? (I acknowledge that those two statements are not mutually exclusive.)

Second–and unrelated, I guess, but you’re already here–I gather that these two are equivalent:

//  block outside parentheses
with(foo) { bar() }

//  block inside parentheses
with(foo, { bar() })

Looking at the signature for with(), the second is what I would expect to see, but the compiler gives a warning, saying the lambda should be moved outside the parentheses. Why?! (I notice that, in the List.fold() example here, the lambda is inside the parentheses; pasting that into the compiler gives the same warning.) If a method takes two arguments, putting one inside the parentheses and one outside rubs me the wrong way!


#2

It is not. with is used for either calling multiple statements with one receiver, or to use member extensions inside the code block.


#3

Let me answer the easy part first

In kotlin you can omit the parentheses around the last argument of a function if it’s a lambda.

with(foo) { bar() }

is the same as

with(foo, { bar() })

This is used to make code more readable. Especially for scoping functions like with, run, let, also and apply this is the preferred way of writing lambdas.

If a call takes a single lambda, it should be passed outside of parentheses whenever possible.

https://kotlinlang.org/docs/reference/coding-conventions.html#lambda-formatting
So I guess the example you found is not following the coding convention as stated. I guess the reason is that they only explain this rule further down the page.


Yes, it’s exactly the same. with is one of the scoping functions in kotlin.
In general they are used to make code more readable. with allows you to call multiple functions or access multiple properties on the same object without having to use it’s name over and over again. It changes the scope from your current to the scope of the object you pass to with. Also this does not change the generated bytecode at all. All it does is give you a more descriptive way of writing code.


#4

Let me elaborate a bit more on why it is “a more descriptive way of writing code”:
Sometimes you have two or three verbose lines of code that operate on the same thing, and the scope of them is very local to those several lines. For example:

fun abc(): Int
fun xyz(): Double{
  val base = 1.0
  val tmp = abc()
  val ratio = if(tmp == 0.0) -1 else (1.0 / tmp)
  return base + ratio
}

Alternatively, the value of ratio could be written as if(abc() == 0.0) -1 else (1.0 / abc()). But wait… this alternative calls the abc() function twice, which might cause undesired performance or semantic effects. But to cache the return value of abc() into tmp? This leads to an extra line of code, and we have to invent a meaningful name for the result of abc(), and using tmp leads to problems if we reuse the same technique later on: duplicate variable declaration (similar to "Duplicate variable i" with multiple for(var i = 0; ...) loops in JavaScript).

So in Kotlin, we could organize this logic into a single statement:

fun xyz(): Double{
  val base = 1.0
  val ratio = with(abc()) { if(this == 0.0) -1 else (1.0 / this) }
  return base + ratio
}

There are other idiomatic ways to do this in Kotlin:

// if you want an object-oriented style
val ratio = abc().run { if(this == 0.0) -1 else (1.0 / this) }

// in my opinion "it" is more intuitive than "this" and prevents context overriding issues
val ratio = abc().let { if(it == 0.0) -1 else (1.0 / it) }

// this involves nullable Double, not sure if kotlin compiler can optimize away Double object boxing
val ratio = abc().takeUnless { it == 0.0 } ?: -1
// or equivalently:
val ratio = abc().takeIf {it != 0.0 } ?: -1

So to conclude: I would use with/run/let if there is a temp variable that I don’t want to think of a name for and will not be used for more than 3 lines


#5

I mostly agree, but I don’t think with/run/let are equally valid here. Of cause they all achieve the same thing, but still. You already point out that “it” is more intuitive than “this” but it goes further.

IMO all of those scoping functions have a different meaning. run implies that I want to run some action on the object, with means that I want to switch the context of my execution to the object.
let on the other hand means, I just want to take this value as a temporary value and do some more calculations on it. Therefor let is the right choice in this situation. (This is my take on those functions, I don’t think this is stated in any coding convention for kotlin)


I’m also not sure about the boxing optimization, but it won’t work anyways as you no longer invert the value. You would need to write

val ratio =  1 / abc().takeIf { it != 0.0 } ?: -1

#7

I’m not sure I agree with you guys about the readability. Suppose we have fun abc(): Foo, and this code:

val unsightlyTempName = abc()
unsightlyTempName.foo()
bar()
unsightlyTempName.baz()

That may not be pretty, but it’s absolutely clear; there’s no question about what’s happening there. Now we replace it with with():

with(abc()) {
    foo()
    bar()
    baz()
}

So clean! But, what is that bar() there? The answer depends on whether the Foo class has a bar() method… and, worse, when that answer changes in six months, this code silently breaks. Doesn’t that risk make with() basically poisonous?

(I mean, that is a real problem, isn’t it? Isn’t it the case that adding a new method to an existing class risks silently breaking every place where objects of that class are used in with()?)


#8

It’s no more risk than a subclass’s risk that a superclass will change the subclass’s context by adding a new method.

I would not use with for simply fixing a bad variable name for two reasons.:

  1. There are other, simpler options that don’t have any context switching risks
  2. with, as Wasabi points out, is for switching contexts

As darksnake mentioned, I often find myself using with to switch contexts that include extension functions:

// In practice, I usually use this as an interface that would be implemented elsewhere. 
// That way I can use extension functions polymorphically.
class SpecialExtensions {
  fun String.hello() = println("Hello $this!")
  fun Int.foo() = this * 42
  fun Double.buzz() = "Buzz of $this"
}

fun main() {
  with(SpecialExtensions()) {
    "Bob".hello()
    println(11.foo())
    println(2.2.buzz())
  }
}

^ As a side note, this is also true for function prefixes like get, find, load/fetch–they all communicate different meanings to the programmer.


#9

Can you give me an example of what you’re talking about? The closest I’m coming up with is if Subclass has a foo() method which does one thing, and then Superclass later adds a foo() method with different semantics… but A) that doesn’t exactly break existing calls to Subclass.foo()–they’re still doing exactly what you wanted them to do when you wrote them six months ago–and B) that can’t happen in Kotlin, right? because the compiler will complain about Subclass.foo() not having override?

Ah, I saw that, but had not read about extension functions yet. Thank you for the example.


#10

You guys are digging too deep for the question asked.
Of course, if you want to do the things the hard way, then with is very important thing in kotlin it allows to define behaviors not in class, but in a lexical scope. In that case the class passed to with is used only for behavior container or even scope marker. If some variant of KEEP-176 will be approved, then it will bring the whole new style of programming (defining behaviors not for receiver, but for other objects in receiver scope).
I’ve recently written an article about it.


#11

To add to the excellent answers already given, I find that I rarely ever use with, preferring run, let, apply and also.

I think it exists for somewhat historical reasons, because early on apply and also did not exist. Once apply and later also were added the need for with was almost non-existent. It might have some good use cases but I wouldn’t focus on it.


#12

Currently something.run{} is equivalent to with(something){}.

If KEEP-176 would pass, than with will have additional meaning, since it will be able to take multiple arguments.


#13

Hmm. I had thought with was like apply. Maybe I should use it more since I kind of like the with version.

But then again I don’t really do much Kotlin programming since I am strictly Java at my day job and side projects in flutter/dart.


#14

Yeah, you are right. apply equals with. run returns a result.


#15

No, you were correct, with is like run in that

with(foo) { ... }

is the same as

 foo.run { ... }

But I remembered that the reason run gets preferred is that you often want to chain it to previous expressions. It is nothing for me to have a sequences of runs and lets.