Solution for nested lambda calls

Nested lambda calls is an issue when using Jetpack Compose or using any Coroutines API take this as an example:

@Composable
fun SomeComponent() {
    LaunchEffect(Unit) {
        launch {
            withContext(Dispatchers.IO) {
                loadFromCache()
            }
        }
    }

    Card {
        someList.forEach {
            Column {
                // this is already deep and we didn't yet written any content
            }
        }
    }
}

My proposed solution is a syntax sugar for the last lambda argument to have another special
syntax for providing it just like somefun(...) { /*body*/ }. The syntax could be anything but it
is a MUST if the Kotlin team is heading more and more towards making Kotlin a content
language (e.g. kotlin-html, kotlin-css, Jetpack Compose, etc.)

My syntax proposal is somefun(...): /* stmt */ where stmt is a single statement just like
in if (...) /* stmt */ and for (...) /* stmt */. This enables a lot of concise code to be written.

Take for example:

for (it in 0..100) if (it % 2 == 0) 
    println(it)

// Woudl be replacable with
0..100.forEach: if (it % 2 == 0) 
    println(it)
for (n in 0..100) if (n % 2 == 0) 
    println(n)

// Woudl be replacable with
0..100.forEach: n -> if (n % 2 == 0) 
    println(n)
listOf(0..100).flatten().asFlow().collect {
    if (it % 2 == 0) launch {
        println(it)
    }
}

// Woudl be replacable with
listOf(0..100).flatten().asFlow().collect: 
    if (it % 2 == 0) launch:
        println(it)
LaunchEffect(Unit) {
    launch {
        withContext(Dispatchers.IO) {
            loadFromCache()
        }
    }
}

Card(modifier) {
    someList.forEach { value ->
        Column {
            Content(value)
        }
    }
}

// Woudl be replacable with
LaunchEffect(Unit): 
    launch: withContext(Dispatchers.IO):
        loadFromCache()

Card(modifier): someList.forEach: value -> Column:
    Content(value)

One last thing, this syntax sugar could be a solution for some ugly code we are used to:

require(someCondition) { "Some message" }

// Woudl be replacable with
require(someCondition): "Some message"
// not saying this is ugly, but it might be better?
implementation(libs.google.tink)

implementation: libs.google.tink // obviously updates in gradle is required too for this to work

More examples:

logger.warn { "Computed message" }

// would be replaceable with
logger.warn: "Computed message"
val someValue  = someScope.async {
    runCatching {
        fetchFromAPI(token, arguments)
    }
}

// would be replaceable with
val someValueResult = someScope.async: runCatching:
    fetchFromAPI(token, arguments)
val someValue = someResult.getOrElse { return }

// would be replaceable with
val someValue = someResult.getOrElse: return

A long time ago, the Kotlin team was discussing decorators, which would be kinda similar I guess. I believe contexts were viewed as a necessary pre-requisite first.
I think the syntax looked something like:

@foo
@bar(x, y, z)
fun baz() {
  // takes place in `foo` and `bar`
}

And, presumably, it’d work with lambdas too:

@LaunchEffect(Unit)
@launch
@withContext(Dispatchers.IO)
loadFromCache()
1 Like

I guess they found a blocked road that complicated things further. :thinking:
But my proposal is just another syntax sugar very similar to an existing syntax sugar that is already implemented and stable for a long time. I am sure it does not require the contexts feature and does not clash with any already existing feature. The gain is many and the loss is none :nerd_face:

Plus, the Kotlin team at that time (I am guessing) were not aware that Kotlin will grow to be a data language as well and would face component-hell just like react and even suspend won’t fix it.

Thus, I am very hopeful the Kotlin team if they reconsider it again with this proposal in mind, they would implement it or at least something similar to it :kissing:

I don’t actually see the improvement. It seems the main difference is replacing curly braces with a colon, but the rest is pretty much the same (?).

But I definitely like the direction @kyay10 mentioned. In this case a big difference is that we flatten the whole chain of calls. Also, special char at the beginning of the line makes it very easy to read, because we clearly see this is a chain.

1 Like

Yes that is the point. No actual change to the compilation process. No introduction of new mechanics.
No changes to execution order. Stay declarative. Expand on an already existing convention. Backwards compatible. Easy to understand. Pretty useful.

I actually don’t think it would be accepted. It makes tracking execution order a pain. It introduces unnecessary syntax. I guess this is why it was dropped.

To clarify I was thinking about this for a long time ago.
I then realized that introducing the pipe operator will be a dream that wont come
and introducing any fancy syntax like that would be faced with a harsh rejection due
to both backward compatibility and the Kotlin team not wanting Kotlin to be overly complex.

Thus, I was thinking about a simpler then simpler solution for the problem.

The Kotlin team has accepted the addition of ..< and case {..} if {..} -> and
then I realized, the common factor in these changes that these are additions that
are just syntax sugar built on top of an already existing features.

Thus, my proposal was introducing nothing but a shortcut. a shortcut that can be utilized in lots and lots of ways.

If I’m following this (which I may well not be), then I guess this is analogous to the way we can use if (and while) either with braces:

if (someCondition) {
    doSomething()
}

or without:

if (someCondition)
    doSomething()

Yes? If so, then it seems there’s ample precedent.

It would also have the same restriction: that the braceless version allows only a single statement; if you want to include multiple statements in the lambda, you need to braces to group them. That restriction is well understood for if and while; does it make sense for the sort of code that might be used in the motivating examples?

There are other restrictions, too: for example, the lambda must be the last thing on the line, so there’s no way to chain lambdas in the way you might do with someCollection.filter{ some condition}.map{ some expression }.

(And is there a risk that unwary people will try to write Python in Kotlin, and get confused when it doesn’t mean the same, no matter how careful they are with the indentation?)

The benefit seems pretty minor in general coding. Is it really significant enough in your case to overcome the default -100 points?

I understand now, it is not about why not. It is about why should we.

From my perspective, there are no downsides.

I understand the restrictions implied you mentioned and I actually think they are important to keep consistent (a feature not a bug). I was aiming for a similar semantic to if (...) /* stmt */ and if (...) { /* block */ } and it would be func(...): /* stmt */ and func(...) { /* block */ } thus no issue here. If you want multiple statements or chaining function calls, use the already existing syntax sugar.

About becoming Python, Kotlin is already clashing conventions with many languages at this point so I guess it is absolutely not an issue to add one more similarity. I think Kotlin at this point has become its own thing. Plus, Kotlin at this point requires an IDE for one to be productive (arguable) or at least one would prefer using a syntax highlighter when writing Kotlin thus the highlighter would guide.

At this point I guess there is no issues. So we are not below -100 but after this comes the issue
of who values what and who does not care. I might value this as a MUST and someone else might value it as a meh. But here are points that might point out why I personally value it as a MUST:

  • Kotlin STD and community libraries tend to rely on last lambda parameters a lot
  • Kotlin becoming a language for writing HTML and CSS and Compose and components
    and styling tend to have a lot of single-child nesting.
  • Introduces a lot of concise ways of expressing code that empowers library authors to ditch
    more complex approaches.

I think that this is the wrong solution to the problem. I think the real solution is to simplify (IE de-nest) your code.

To me, this is the same as having a very large function. If there’s too much code in a function, it can become hard to follow the logic. If there’s too much lambda nesting, it can be hard to follow the logic. Break the code up.

2 Likes

I totally agree with @Skater901. My first JetPack Compose code looked like the pyramids of Gizeh sideways. I started to refactor, write helper methods, and now it is not only much more readable, but also reusable. I think the Compose library could do a better job to provide some common abstractions out of the box, but that’s not a problem of the langauge as such.

I also dislike the suggested syntax. While it is shorter, I’m pretty sure it will lead to confusion and weird edge cases. Having an explicit end of a block might look ugly, but gives you much more security, at least when you want to keep the rest of the language as it is. The suggested syntax fits only in languages which have already a strong preference for semantic indentation. Kotlin isn’t Python with types, and there is no sane path to get there syntactically, even when assuming that it would be desirable, which I find questionable.

1 Like

Splitting to smaller code blocks is a common good practice, but how would you like to split this:

suspend fun copyFile(src: Path, dest: Path, timeout: Duration) {
    withContext(Dispatchers.IO) {
        withTimeout(timeout) {
            runInterruptible {
                src.inputStream().use { input ->
                    dest.outputStream().use { out ->
                        interruptibleStreamCopy(input, out)
                    }
                }
            }
        }
    }
}

Example may seem unrealistic and created specifically to proof something, but it sometimes happens to me. Once, I found a library for Kotlin with sole purpose of flattening chains of use { } calls, because sometimes we have 3-4, which is annoying. Other common “chainable” functions: measureTimeMillis, runCatching, forEach, or some custom like: transactional, retry, etc.

By using the syntax mentioned by @kyay10 , it would be something like:

suspend fun copyFile(src: Path, dest: Path, timeout: Duration) {
    @withContext(Dispatchers.IO)
    @withTimeout(timeout)
    @runInterruptible
    @src.inputStream().use // not sure for the syntax to receive args
    @dest.outputStream().use
    interruptibleStreamCopy(input, out)
}

For me this is a huge improvement. Of course, because this function doesn’t do too much else than chaining, even the original one is a kind of fine. But in practice, in the middle of a function we have a chain of 3 calls, then a longer block of code inside it and it is all indented by 12 spaces without any reason. Still, the code is too simple to split it into multiple functions.

But again, I don’t see how the suggestion by the OP could improve anything here. It replaces { with : which at least for me is pretty much the same. Lack of closing brackets could be considered an improvement (or the opposite).

Btw, since the syntax I discussed is getting mentioned:

  1. I don’t remember if the Kotlin team actually discussed that the @ syntax would be useable inline. I know for a fact that syntax was showcased for function declarations though.
  2. I like that syntax, but I think an alternative but very similar syntax might be better. If you notice, the examples so far use a single statement, but I (personally) don’t like that there’s no visual indicator that the statement is running in some context, so perhaps something like:
suspend fun copyFile(src: Path, dest: Path, timeout: Duration) {
    @withContext(Dispatchers.IO)
    @withTimeout(timeout)
    @runInterruptible
    @src.inputStream().use
    @dest.outputStream().use
    run {
        interruptibleStreamCopy(input, out)
    }
}

would be better. The point is that you still get the flattening, but you get obvious separation between the decorated vs non-decorated sections. Also, presumably that function could be rewritten as:

@withContext(Dispatchers.IO)
@withTimeout(timeout)
@runInterruptible
// should there be `suspend` here? On the outside it's needed, but it's not available on the inside...
suspend fun copyFile(src: Path, dest: Path, timeout: Duration) {
    @src.inputStream().use
    @dest.outputStream().use
    interruptibleStreamCopy(input, out)
}

or maybe even allowing src and dest to be referenced in the decorators (i.e before they’re declared)

1 Like

My assumption is that decorating itself never involves indentation. In most cases the last call would be a regular lambda call which introduces a usual { } section and indentation:

@withContext(Dispatchers.IO)
@withTimeout(timeout)
@runInterruptible
@src.inputStream().use
dest.outputStream().use { out ->
    // some code
}

Only because we have this convenient interruptibleStreamCopy function already, we could do it differently in this case. But I see your point. It is inconsistent if some lambda calls are done one way and the last is different, so maybe such run at the end is a good idea. As a matter of fact, even if not having this special run syntax, your syntax is still valid, because we have a run function already. Magic of Kotlin :smiley:

1 Like

I am totally with you when it comes to the usual programming problems!

But, when it comes to designing (e.g. writing HTML or Jetpack Compose) this becomes hell.
You can’t breakdown everything, especially in design when it is required to put things in Row then Column and wrap it in some LocalCompositionProvider and wrap all that in a Card with a Box because Card does not go well with clickable on its own.

I think if we continue telling ourselves that the issue is with our refactoring and the component hell
is not an issue we will eventually find ourselves either not refactoring at all or wasting time with Go to definition calls to utility components that just wraps one or more components together.

In UI design, large code CANT be escaped.

This argument actually helps my case. If lambda nesting was just to wrap a component, then
anyone reading would get it rightaway (that is why I did ask for a single statement restriction) but
when your code is “split up” you find yourself walk all around the codebase for one component that have to be nothing but a big component.

Writing one helper is fine. But writing lots of helpers for each combination a person might wish for
when designing UI is not a good approach in an application codebase and not a possible thing to create a library for it.

I don’t very like the suggested : either. It is used to define type annotations.
But, there must be another character we use because we can’t just leave it
since it would confuse the lexer if that statement was the next statement or the last lambda parameter of the previous statement. But I did submit it anyway because the idea is more important. Here are other characters that might do the trick:

// original proposal
func(...): /* stmt */
func(...): -> /* stmt */
func(...): _ -> /* stmt */
func(...): _, _ -> /* stmt */

// another proposal (original without ':') challenging, but is my preference
func(...) -> /* stmt */
func(...) _ -> /* stmt */
func(...) _, _ -> /* stmt */

// using the `|` symbol (not recommended)
func(...) | /* stmt */
func(...) | _ -> /* stmt */
func(...) | _, _ -> /* stmt */

// using the `<<` symbol
func(...) << /* stmt */
func(...) << _ -> /* stmt */
func(...) << _, _ -> /* stmt */

// other might do the trick as well but I guess the point is clear now

I never wanted that and I think we should not be concerned about someone thinking that.

Finally

This proposal is solely for fixing nested lambdas which happens a lot in UI design and when using any Kotlin library at this point.
Breaking up code is a workaround not a solution in the particular use case of Kotlin (designing UI).
Using the syntax @kyay10 mentioned is first, needs changing the @ symbol since it is already in use for annotations. Second, is the exact same proposal as mine but with a prefix @ instead of a suffix : and no way of obtaining lambda parameters _, _ ->

I believe the Kotlin team mentioned that it’d be distinct enough since it’d be a function call, which starts lowercase, vs an Annotation, which starts uppercase. I’m not married to the syntax though, just mentioning that there’s already some consideration given to this idea.

That’s another interesting problem. Maybe there can be some way of defining parameters in the @ syntax. Another way it can be done is with contexts.
I agree overall that such a solution is needed. I just think that perhaps it needs more design than just being nice syntax.

Something different to give another viewpoint. In the Context Parameters KEEP, there is a discussion for adding a “bring into scope” keyword, for example:

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

which would be equivalent to the current:

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

Now, that is a much simpler problem because delimiting where it starts and ends doesn’t matter. The only thing it’s doing is bringing something into scope, it doesn’t change the behavior of the code, and nothing happens at the end of the scope, so the fact that the end of the scope may be slightly ambiguous is irrelevant.

However, I do think we should wait for this to be implemented and look at its result to decide how decorators should be implemented. Whenever that is implemented, I wouldn’t mind a:

suspend fun copyFile(src: Path, dest: Path, timeout: Duration) {
    decorate withContext(Dispatchers.IO)
    decorate withTimeout(timeout)
    decorate runInterruptible
    decorate src.inputStream().use input
    decorate src.outputStream().use output
    interruptibleStreamCopy(input, out)
}

Note that the scoping would be different than the contents of this proposal: instead of the decorator applying to very next instruction, they would apply to the entire parent scope. I much prefer that rule: it doesn’t magically execute some code at the end of the next expression, where there are no syntax marking. This also avoids the needs for distinct method decorators and is more coherent with Kotlin’s rule of keyword positioning around method declarations.

Ultimately, the goal is to reduce nesting.

1 Like

When you put it this way. It kinda make sense to me now.

Question: is the “decorate” keyword affects the entire function or just the next statement?

Question: if it does affect the entire function, how to reduce it to a single statement?

Question: what so different in making the “decorate” keyword a suffix instead of a prefix?

This is very reminiscent, in fact nearly identical, to Koka’s with syntax:

suspend fun copyFile(src: Path, dest: Path, timeout: Duration) {
    with withContext(Dispatchers.IO)
    with withTimeout(timeout)
    with runInterruptible
    with input <- src.inputStream().use
    with output <- src.outputStream().use
    interruptibleStreamCopy(input, out)
}

In this version of the syntax, it would affect the entire function, but you can limit it by e.g. surrounding your single statement in a run block.

I think it being a prefix signifies that “something special is going on” better probably, although it really is up to taste

In my mind, it starts where the decorate keyword is, and stops where a variable declared on the decorate keyword would go out of scope.

For example:

fun foo() {
    decorate whatever // entire function

    with (bar) {
        decorate baz // the entire contents of the 'with' block

        println("foo")
        decorate bar // from here to the end of the 'with' block
        println("bar")
    }
}

Use any kind of scoping mechanism that would otherwise reduce to a single statement, e.g. run {}. Basically, you’d create a scope, but the other way around: keep the first lambda using lambda syntax, declare the others as decorators:

withContext(Dispatchers.IO) { // keep this one to declare a scope
    decorate …
    decorate …
    decorate …
    … your statement …
}

I’m not sure what you mean here. All Kotlin keywords are prefixes. The thing that’s different in this version of the proposal is that the “thing that removes nesting” doesn’t magically create lexical scopes, but instead reuses the ones that are already declared in code.