Is `with` really good, idiomatic Kotlin?

You did understand me correctly. Translating that to Kotlin would mean to change the implementation of with, so that it wouldn’t use an implicit receiver.

I think this question is basically the same as asking:
“Is it really good, idiomatic Kotlin?”

I use idiomatic to mean, understood by the natives. As Kotlin coders, scope changing functions are without a doubt idiomatic.

OP really is asking if using with, in a well understaood and idiomatic way, is actually bad practice.

I do think there’s some merit to the question.

Even those in this thread who say they’re against using with show that there are both good and bad uses (just like it). Specific examples don’t reveal much since showing a hammer can’t drive a screw doesn’t make the hammer a bad tool.

Both it and with have options to make their use more clear. For it we have the two options of adding a name, or a name and type. As for with we have labels (and the option to switch to let). Intellij adds hinting labels (forgot the real name) that explicitly call out the new reciever.

I think at this point it would be difficult to enforce Kotlin coders to write code in a way that’s easy to read without an IDE. Java has the advantage here because Java finds its clarity in its verbosity.

Kotlin gives coders the option to write confusing code using: type inference, variable shadowing, it param, lambdas with recievers, generic arg inference, and more.

This is done to find a practical balance. As we know with Java, verbosity doesn’t always lead to more readable code.

Kotlin leans on it’s tooling for major benefits. A coder can provide a readable API while getting to piggyback on Kotlin tooling–it’s the only way Kotlin DSLs are usable, they’re tooled for free.

I’m not fare to judge many of Kotlin features through a narrow use of zero tooling; it’s important to remember both use-cases. Sometimes the best solution to an issue is an IDE hint.

Kotlin should not try to grow in a manner that prioritizes tool-less users too highly.


Slightly off topic (oops :man_shrugging: ):

On the note of non-intellij users, missing those type hint labels (still forgetting the name), it’s definitely a loss.
IMHO, if the cost isn’t too high, I think a big bonus to break down the barrier of adopting Kotlin from those who use text editors instead of IDEs (and don’t know what they’re missing), would be to find a way to provide those labels and non-smart syntax highlighting in a broader set of those tools (terminal editors, emacs, vi/vim, and text editors, atom, vscode, etc).
Not full refactoring or anything fancy, just the labels and non-smart highlighting. IMO having hint labels everywhere solves a large chunk of the with issue.

Or maybe some kind of embeddable config I could check into VC to provide that support–similar to checking in .idea folder.

3 Likes

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.

5 Likes