Is `with` really good, idiomatic Kotlin?

The question comes down to does with (and similarly run and apply) make code harder to read or easier to read? And the answer is a definite YES. It definitely can be abused to make code harder to read, just as any language feature can. It also can make code MUCH easier to read and less tedious to write. The whole notion of functions and lambdas with a receiver is extremely powerful and allows for things like DSLs.

Applying your logic to the real world, you would advocate that we should outlaw automobiles because even though they allow great mobility and good in the world, they can be used as killing machines like that terrorist who killed 86 in Nice, France in 2016.

The job of a programmer is to write code that computers and humans can understand. As Martin Fowler said, “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” You are advocating removing of a feature that a good programmer can use to accomplish that.

Take the original example in this thread:

with(Turtle()) { //draw a 100 pix square
    penDown()
    repeat(4) {
        forward(100.0)
        turn(90.0)
    }
    penUp()
}

I made a couple modifications which were to eliminate the unneeded variable and use the simpler repeat function. That is one of the advantages I find with the whole with, run, let, apply, also family is the elimination of unnecessary variables which can lead to easier to follow code. Naming things is hard and the less things you have to give names to can make code easier to understand.

So here is what that code would look like without implicit receivers:

myTurtle = Turtle()
//draw a 100 pix square
myTurtle.penDown()
repeat (4) {
    myTurtle.forward(100.0)
    myTurtle.turn(90.0)
}
myTurtle.penUp()

This version adds so much noise that it is much harder to read. You can’t see the forest for all the trees. Yes it makes it clear what each function is called on but it would be difficult to say this is better. I would probably in such a case add extension methods on Turtle to even drop the with and make a Turtle DSL. For example the whole penUp, penDown thing is cumbersome and error prone to manage the pen state. I would probably do:

inline fun Turtle.draw(block: Turtle.() -> Unit) {
    try {
        penDown()
        block()
    finally {
        penUp()
    }
}

Turtle().draw {
    //draw a 100 pix square
    repeat(4) {
        forward(100.0)
        turn(90.0)
    }
}

That is all made possible with the concept of functions with receivers which you are arguing against.

I too in the beginning didn’t find a reason for with but now I probably use it more often than run (which is the equivalent of with). I see different connotations to the two. With is used where I have a larger block of code where I don’t want to use a named variable that I have to keep repeating the variable. I would use run for very small inline transformations on an object (see this post of mine for an example).

Also relevant to this topic was a proposal (sorry, couldn’t find the post) to allow explicitly declaring a lambda with receiver. The proposal was to allow lambdas of the form { this -> ... } to explicitly notate that this lambda is redefining this.

6 Likes