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.

6 Likes

In the curse of this discussion, I came to the conclusion that with is a double-edged sword. There are good use cases but also bad use cases. Since a colleague of mine produced a lot of bad use cases, I wondered whether this features does more harm than good in general. But there seems to be no simple answer. However, the related sections in the documentation should probably provide some guidelines when and how to use scope functions. Maybe we should focus the discussion on these guidelines.

My suggestions for using scope functions like with, let, apply and the like:

  • Don’t use it with more than one level of nesting
  • Use it only, if it is crystal clear what function is called on what receiver
  • Use it only for small code blocks (up to ~ 10 lines of code), possible exception: DSLs
  • Control the scope of implicit receivers with @DslMarker
  • Use explicit parameter names instead of it if the code block is larger or contains nested scope functions
2 Likes

I think we’ve all experienced a seeing someone misusing/abusing something to the point of wishing that thing was disallowed just to avoid dealing with their misuse :stuck_out_tongue:

I can imagine someone misusing with because it “feels” slick and saves characters. My general strategy to help discourage this kind of misuse of language features and personal coding “tricks” that “help” (if you know what I mean :man_facepalming:) is to advocate for one code trait above all else: Clarity for the reader.

^ I found putting the emphasis on “for the reader” part seems to help get it through to people. After all, in writing applications, we code for the reader over the writter (scripting can put the writer over the reader though since we plan on throwing that code away ASAP).

2 Likes

While I like those rules I don’t think they are really necessary for just scoping functions. If you look at any guide on how to write good code you get a similar list

  • No deep nesting of scopes (if, loops, lambdas, etc). How deep is too deep is something debatable but normaly 1 or 2 levels are fine, anything more is considered bad
  • Be explicit if it helps understanding. Short code is nice but only if the reader can still understand it
  • Keep functions short (up to 10 lines of code, it should always fit on 1 screen) and when possible split it up into multiple functions
  • Use descriptive variable names to aid readability

The only issue with scoping function is that the reciever of an expression can be “hidden” from the reader but I’d argue that still falls under my second point.

Basically I agree with most people here that every feature can be used to write bad code. Some more, some less and what we really need are good guidelines, we can always break them when necessary :wink:.
I like @arocnies’ “clarity for the reader” paradigm, it pretty much combines everything we’ve been talking about here :smiley:

1 Like

I’d chime in on this as well. My attitude to coding is to start from the premisse of writing the code in the way that I want to express the problem in the clearest way possible. Then you can create functions, extension functions, extension properties, types, etc. to make it possible to write that.

In that sense you find that sometimes you want to write trivial functions that do some conversion (say from mouse coordinates to cell coordinates in a grid). They are just a single division and more writing than putting the division in the code directly. However, they make it much easier to read the code and know what is going on. And then you trust that speed isn’t an issue, and that most of this gets optimized away anyway.

1 Like

I think it is fine to point out that this feature can be abused and lead to less clear code and to point out the ways it can make code harder to read. But I don’t like the idea of putting numerical boundaries on those uses. I have probably violated all of those because it was clearer and less cumbersome to read.

And I can tell you from experience working in Dart lately that doesn’t have any of this other than the ability to create some extensions that it is very painful not to have this capability and it is one of the things that make Kotlin great.

To me it is no different than accessing member properties in a class. You don’t need to qualify them, but you could by prefixing them with this. every time to make it verbose where it is located, but that generally does not lead to better code.

1 Like

Nice comparison. That potential readability issue is solved with color highlighting. Maybe a similar color-changing/highlighting fix could help with the potential readability of scope-changing functions?

So far we have solutions of:

  • Some kind of in-language label (taken from it)
  • IDE color highlighting (taken from member properties)
  • IDE hint-label (current solution. On by default I think)

For some reason it took me two months to think of a reply, sorry. :slight_smile:

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.

Perhaps it’s fair to say that you want Ada’s renames functionality in Kotlin. I could imagine two ways to introduce this. One, of course, is to introduce a proper renames function. Another would be this:

with (T = myTurtle) {
    T.penDown()
    for (I in 1..4) {
        T.forward(100.0)
        T.turn(90.0)
    }
    T.penUp()
}

Or, let’s take another example:

with (x = object.property.subproperty.yetanother.field) {
    // do a bunch of stuff with x directly
}

Do I understand correctly that something like that would satisfy you?

I don’t know enough about how Kotlin’s innards work to say whether that’s feasible, but personally that’s something I’d like a lot, too. I’ve often written something like this:

val a = longVariableName
// do a lot of things with a

…and Kotlin’s compiler warns me that this declaration could be inlined. I usually end up using longVariableName instead. I was unaware of Kotlin’s with function, so I suppose I could use that. But I’d rather use a version of with as shown in the second example, and I’m leery about using it as in Kotlin. Sorry if that bugs others.

(I’m in a bit of a time crunch, so sorry also that this is probably not well explained or possibly even well thought through.)

In this case, you can simply do:

longVariableName.let { a ->
    // Do stuff with a
}
1 Like

I think you’re addressing my concern. Yes, that does address my interest. I was unaware of scope functions before this discussion. Personally, that seems awkward to me, a needless use of multiple symbols (braces, arrow) when a word would do, especially if I have to do it several times; I don’t want to deal with so many nested braces. I would rather have something like Ada’s renames or, honestly, just val a = longVariableName without a complaint that the declaration could be inlined.

But, I can at least work with that, so I appreciate your pointing it out.

1 Like

You could edit the IDE settings to disable the inspection (Do Alt+Enter and then Disable Inspection), or add a @file:Suppress("UnnecessaryVariable") to the top of the file

You could edit the IDE settings to disable the inspection

I don’t consider that a good idea in general; I like to release my code as open source, so if someone else wanted to edit it, they would

That said, a lot of Kotlin’s warnings are excellent and provide great suggestions for learning the language and improving the code, but this one is silly. Renaming variables in order to provide a clearer meaning of its utility in a particular context contributes to readability. So I may suppress the warning on a case-by-case basis.

1 Like