Chain invokes with lambda arguments

Simplified code:

    object B {
        operator fun invoke(arg: () -> Unit) {}
    }
    object A {
        operator fun invoke(arg: () -> Unit) = B
    }
    fun f() {
        (A {}) {} // ok
        A {} {} // error "Only one lambda expression is allowed"
    }

Perhaps it would be more useful to interpret the second case the same way as the first one instead of generating an error?

1 Like

What if there are 2 overloads, one of which taking 2 arguments?

Also, this is a quite contrived, oversimplified example. If the lambdas actually contain code, especially multi-line code, this is going to be quite confusing even in the case without overloads. I think neither option is nice in that case. You should rather prefer extracting the first call into a variable that you name with something that makes sense, and then call that variable as a function:

fun f() {
    val doTheThing = A { ... }
    doTheThing {
        ...
    }
}

IMHO, main source or confusion is usage of operator invoke in regular code ))

But it’s very handy in various DSL’s, where someone may prefer A { } { } syntax.

two arguments must be separated with comma or () {} construction, not {} {}, how does this relate to this case?

upd: Finally i found a workaround without bracketing the first fragment, A {}() {} but it’s still ugly )

two arguments must be separated with comma or () {} construction, not {} {} , how does this relate to this case?

Fair enough, my first point was moot.

However, my most important point is the second.

But it’s very handy in various DSL’s, where someone may prefer A { } { } syntax

I don’t believe a DSL with 2 consecutive blocks is very readable (especially when their contents is multi-line, but even on one line) because it’s not very clear which lambda plays what role.

I think the best for such DSLs is to use an infix function instead of invoke() in order to keep the chain effect but provide names in between: A { } something { }. This solves the weirdness of having 2 blocks without clear semantics.

3 Likes

I don’t agree, because

x {} {}

is basically the same syntax like

x () ()

but you’re likely not putting as much text inside each of the parentheses than the braces. As soon as it’s a multiline instruction, you’ll end up with a line

} {

and what does that represent? Will it be clear why that line is where it is, and why the two lambdas are separated like that? I doubt it.

  1. in a DSL it’s clear what it means

  2. you think } ) { looks better?

as in

(xyz {
   line1
   line2
   ...
} ) {
   line1
   line2
   ...
}

I think this is more clear:

xyz {
   line1
   line2
   ...
} {
   line1
   line2
   ...
}

that’s one of my personal favorites. Usually, if the same thing ()() vs {}{} is handled differently there it will have it’s side effects

and this doesn’t matter, either, simplicity, less rules to describe a difference that doesn’t really exist, consistency, orthogonality, enough reasons

language design is not about use cases or taste

e.g. look at python, list comprehensions and similar syntax is backwards for no reason, totally unreadable, just because someone thought it would be nice to imitate natural language

it’s more about reducing the number of rules to describe the language.

For example (again python) languages that need to distinguish statements from expressions get into trouble all the time (e.g. lambdas, that can only be expressions, why python?)

1 Like

Yes! It’s my main point.

( “somewhere behind” is ugly and must be avoided when possible.

(one(arg1).two(arg2) ?: value).three(arg3) is bad

and one(arg).two(arg2).let { it ?: value }.three(arg3) is much better.

It doesn’t matter if x { } { } is “good” or “bad” practice. It fits logically into the language design.

(x { }) { } is much worse.

x {} () {} solves “( somewhere behind” problem but it’s still ugly.

Your response has done nothing to convince me that there is a legitimate use case for this syntax. None of you have provided an actual example where it’s obvious what the two lambdas each stand for.

1 Like

IMHO, no one will be able to provide example of legitimate use case of () () and even of operator invoke itself in general.

There will always be an informative function name instead of ().

Meanwhile () () is valid construction and {} {} is not. Does it make sense?

IMHO, this is the case when simplicity and uniformity of code interpretation by a compiler is more important than applicability of special cases

maybe this example is less ‘oversimplified’.Still not real code but maybe it’s more informative

interface RpcCaller<InterfaceType> {
    operator fun <MethodReturnType> invoke(call: (InterfaceType) -> MethodReturnType): MethodReturnType
}

interface Some {
    interface Options {
        var opt: Int
    }
    operator fun invoke(tuner: Options.() -> Unit): Int
}

interface Other {
    operator fun invoke(arg: Int): Int
}

interface SomeRpc {
    fun a(arg: Int): Int
    fun b(arg: Int): Some
    fun c(): Other
}

fun someFun(caller: RpcCaller<SomeRpc>) {
    val a = caller { it.a(1) } // ok
    val c = caller { it.c() } (arg = 0) // this works too!
    val b = (caller { it.b(2) }) { opt = 1 } // ugly workaround
    val b1 = caller { it.b(2) } () { opt = 1 } // less ugly workaround
    val b2 = caller { it.b(2) }.invoke { opt = 1 } // one more workaround
    val b3 = caller { it.b(2) } { opt = 1 } // error.
}

You could replace:

.let{ it ?: value }

with the more readable:

.orElse(value)

if you defined something like:

public inline fun <T : Any> T?.orElse(value: T) = this ?: value

(Or if that were added to the stdlib as per this ticket…)

1 Like

At the risk of getting sidetracked, there’s at least one common one: factory methods that look like constructors.

(Define an operator invoke(…) method in the companion object, and you then have something which is called exactly like a constructor, but can perform arbitrary operations before calling the primary constructor, can return a cached value instead of a new instance, can return a subclass, avoids many of the gotchas around order of initialisation, etc. — Of course, if a class has many of them, or their meaning isn’t obvious, then named methods would probably be better. But IME there are many cases where a simple operator invoke method is clean and simple.)

You’re right, but there’s a minor flaw

public inline fun <T : Any> T?.orElse(value: T) = this ?: value
fun c() {
    val a = 1.orElse(2) // no warnings
    val b = 1.let { it ?: 2 } // useful warning about non-nullable `?:` argument
}

while it’s true in this simple case, it’s also about statements vs expression.
I prefer to “store” some intermediate values into descriptive names, before returning the the result expression,
instead of a complicated expression, where you don’t see what each term means