Multi-lambda functions look quite clunky. Is there...
# language-evolution
y
Multi-lambda functions look quite clunky. Is there a chance some form of multiple-trailing-lambdas could be added to the language? It might be difficult for the parser to understand, I guess. Something good to note is that it is possible right now, but without the possibility of `inline`ing easily:
Copy code
@JvmInline value class PartiallyProcessedResult<T>(val result: Result<T>)


inline infix fun <T, R> Result<T>.myFold(ifSuccessful: (T) -> R): PartiallyProcessedResult<R> = PartiallyProcessedResult(map(ifSuccessful))
inline infix fun <R> PartiallyProcessedResult<R>.ifThrowable(ifThrowable: (Throwable) -> R): R = result.getOrElse(ifThrowable)

// allows
result myFold { ... } ifFailure { ... }
This example is easier to figure out than other more-complex use cases (e.g. if both lambdas need to be available at the same time, then there's no option to
inline
, and instead one must pass lambdas around). This would also allow some nice SQL-like DSLs EDIT: removed bad initial suggestion of multiple lambdas with no infix in the middle
Not sure how overload resolution would work in the first or second example, but I think it's very rare that there would be 2 overloads with different number of trailing lambdas where the one with less lambdas returns something `invoke`able, so even if the compiler just reports an ambiguity if there's any overloads of that form, I'd be happy with that
j
I find multiple trailing lambdas way too hard to read. When passing multiple lambdas, I definitely expect people to use named parameters anyway. Your last example can be done by naming the infix function
ifFailure
instead of
invoke
, and it's more composable too
y
I'm fine if only the infix-like syntax would be available. The issue here is that such a
myFold
function cannot be defined "in one go", as in any data would need to be passed along in the resulting type of
myFold
and onto
ifFailure
. I also cannot enforce that
ifFailure
would be called at all. I had to introduce an intermediate type
PartiallyProcessedResult
in this example to nudge the autocomplete to tell the user to call
ifFailure
j
Yeah if passing
ifFailure
is not optional, then why not just use named parameters?
y
What I'm imagining is something like this:
Copy code
inline super-infix fun <T, R> Result<T>.myFold(ifSuccessful: (T) -> R, ifFailure: (Throwable) -> R): R = fold(ifSuccessful, ifFailure)
So that I can do a call like
Copy code
result myFold { ... } ifFailure { ... }
Using named parameters makes the syntax a lot worse:
Copy code
result.myFold(ifSuccessful = { ... }, ifFailure = { ... })
It's a similar question to "why have infix": It's there to make the syntax a bit nicer, especially for DSLs
My point is that this is possible but with juggling of data around, and hence I'd like a supported way to do it without juggling and with proper
inline
support (not only for performance, but to allow contracts and non-local returns)
h
Swift uses the same syntax and I think it’s confusing. You (and the parser!) don’t know exactly when the function ends, IDE support is limited, reading without IDE is hard too and it clashes with infix functions.
j
Using named parameters makes the syntax a lot worse
In general I use named params for this
I don't know how Kotlin could deal with
result myFold { ... } ifFailure { ... }
and
infix
function existence.
y
@hfhbd do you mean multiple trailing lambdas in general? Or just the infix-style?
@Javier I'm fine with the compiler erroring if I have 2
myFold
overloads like that. I think it should maybe always prefer one or the other, but again it doesn't matter in most situations
h
Multiple trailing lambdas
j
myFold { ... }
and this is a problem, why the first lambda does not include the named param and the second one includes it? Should it be
Copy code
result myFold ifSuccessful { ... } ifFailure { ... }
It can be easy to understand in a fold like function as the context is successful or failure, but what about two random lambdas in a row in a composable function?
y
I think the naming of the function itself can be sufficient. But sure, having all the names mentioned is fine by me. There is however the risk of it all looking like word soup
j
It could be weird in multiline too
Copy code
myFold(
    ifSuccessful = { },
    ifFailure = { },
)

myFold
    { }
    ifFailure { }

myFold
    ifSuccessful { }
    ifFailure { }
I have had problems with infix functions when they become multiline with Kotest in the past, for example, it is easy to have a problem when chaining. Sadly I don't remember which was the issue but I remember moving everything to non-infix style in multiline.
h
I do see a very very limited use case but not as a language feature. What about assign the result to a variable? This looks weird to me, same for passing this function call to a function parameter. Parentheses are a very simple but useful helper to improve readability. Same reason why Kotlin requires parentheses for if/when too, unlike Swift.
j
> I think the naming of the function itself can be sufficient I don't think so
Copy code
@Composable
fun SomeUiElementWithMultipleClickableItems(
    onIconClick: () -> Unit,
    onTextClick: () -> Unit,
) { ... }

SomeUiElementWithMultipleClickableItems
    {}
    onTextClick {}
y
I'm thinking formatting it like:
Copy code
SomeUiElementWithMultipleClickableItems {
  ...
} onTextClick {
  ...
}
So that it is similar to a
try-catch-finally
or an
if-else
To your point @hfhbd, this is perfectly valid Kotlin:
Copy code
val foo = try { ... } catch (e: Blah) { ... }
I do agree that this is a niche use case. Frankly, if this takes significant dev time, it's not worth it at all!
j
I think ktfmt would go use the other format based on how they already format similar code
Anyway it is different compared to what Kotlin style uses
j
You can workaround it by putting a default no op argument at the end, like
_unit: Unit = Unit
w
You might be interested to know that this is currently possible in Swift quite neatly:
Copy code
func myFold(_: () -> (), ifFailure: () -> ()) { }

func foo() {
    myFold { ... } ifFailure: { ... }
}
In theory Kotlin could implement this through named arguments. But AFAIK currently there is no way to effect overload resolution by parameter names, and I doubt they are eager to change this. Additionally, it wouldn't be as flexible as Swift would be:
Copy code
func foo() {
    myFold { ... } ifFailure: { ... }
    myFold { ... }
    ifFailure: { ... }
    // Both fine in Swift
}
But in Kotlin:
Copy code
func foo() {
    myFold { ... } ifFailure = { ... } // Could in theory be implemented
    myFold { ... }
    ifFailure = { ... } // Would be source breaking change if this would resolve to parameter instead of var/property setter
}
Additionally, I think that using inline functions as an argument isn't going to be as convincing, since it's a micro optimization that will be solved by JIT on hot paths anyways.
y
Inline functions can do non-local returns though, and they pass
suspend
and
@Composable
through. That's a very powerful argument for why we need them
j
True, but I still don't see a very compelling argument for adding a language feature just to write
myFold { ... } ifFailure { ... }
instead of
myFold(ifSuccess = { ... }, ifFailure = { ... })
. I don't think it passes the -100 points rule to begin with, but also I don't think it is actually desirable from a code style standpoint.
w
> Inline functions can do non-local returns though, and they pass
suspend
and
@Composable
through. That's a very powerful argument for why we need them Yes, but this will still work in both the infix case as well as the named parameters case in the current form. The difference isn't the inline function semantics. AFAIU the use case (aside from much nicer syntax)* is micro optimizing intermediate objects. Which will be eliminated by escape analysis on hot paths. Don't get me wrong, I just enjoy discussing design and I try to share some arguments I think one would have to defend/improve if you want to turn this into a KEEP 🙂
y
Oh I see what you mean. I thought the argument was that the infix-style can be achieved now with infix already, and so my rebuttal was that it stops inlining (e.g. if you had a black-box inline function you needed to call), but yes named parameters and the proposed style have identical semantics
👍 1
c
In general, the idea of using Kotlin for “DSLs” was very popular earlier in the language’s lifetime, but more recently it seems like the community generally prefers named parameters to custom DSLs or lesser-used language syntax like infix functions. Sure, it’s kinda cool that you can do custom DSLs like Kotest or Kotlinx.HTML. But a “cool factor” doesn’t necessarily mean that it’s useful or better than the alternatives. For example, the time-tested Builder pattern with named parameters is almost always going to be easier for a library to implement, is more natural for developers to use, and easier to understand at a glance without needing to know too much about the specifics of the Kotlin language.