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?
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 {
...
}
}
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.
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.
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?)
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.
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.
}
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.)
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