What's the benefit of Kotlin's trailing lambda convention?

If the last argument after default parameters is a lambda, you can pass it either as a named argument or outside the parentheses:

fun foo(
    bar: Int = 0,
    baz: Int = 1,
    qux: () -> Unit,
) { /*...*/ }

foo(1) { println("hello") }     // Uses the default value baz = 1
foo(qux = { println("hello") }) // Uses both default values bar = 0 and baz = 1
foo { println("hello") }        // Uses both default values bar = 0 and baz = 1

In Scala we do this very explicitly, should scala support this?

IMO there is no benefit. I think Kotlin allows for this because adding multiple parameters lists is not possible and currying requires a lot more boilerplate.

I think Scala already is better in this regard and does not need any changes. Being explicit is barely any longer or inconvenient and a lot easier to understand (in my experience of course).

17 Likes

This is not directly related, but since we’re on the topic of lambdas and Kotlin, one thing I like in Kotlin is this idea of omitting the arguments and the lambda arrow if not needed (e.g., tabulate and fill are the same function). Would that constitute a less confusing alternative to by-name argument?

Could you given a full example ?
I’m not sure I understand what you mean

List(5) { i -> "X".repeat(i + 1) } // tabulate
List(5) { Random.nextInt(1, 11) } // fill

Both expressions use the same function. You just don’t write i -> or _ -> in the second expression because you don’t use the argument. In particular, when there’s no argument, you simply drop the () -> part, making thunks look like by-name argument on the caller side.

Thank you for clarifying, I was however asking @charpov (who replied)

For my opinion on the original question, I direct you to @bmeesters’s answer with which I agree 100%.

I may be missing something but isn’t the same effect achieved in Scala just by currying?

2 Likes

Modulo empty parens.

def foo1(
    bar: Int = 0,
    baz: Int = 1,
    qux: => Unit,
): Unit =  {  }

def foo2(
    bar: Int = 0,
    baz: Int = 1
  )(
    qux: => Unit,
): Unit =  {  }

foo1(1) { println("hello") }           // error
foo1(qux = () => { println("hello") }) // ok
foo1 { println("hello") }              // error

foo2(1) { println("hello") }           // ok
foo2(qux = () => { println("hello") }) // error
foo2()(qux = () => { println("hello")})// ok
foo2 { println("hello") }              // error
foo2(){ println("hello")}              // ok
1 Like

Yes, but don’t know why Kotlin choose such a limited way.

1 Like

What if Random.nextInt(1, 11) itself returns a Int => A?

In Kotlin it would be a lambda returning Int => A. Braces in Kotlin in expression position always means lambda. This is why adopting that behavior in Scala is not possible.

1 Like

Does that mean that Kotlin allows no local blocks? As in

val x = {
  val y = 2
  y * y
}

or in

f({ val y = 2; y * y })

?

Kotlin has no local blocks as far as I understand.

x would have Type () -> Int in your case.

fun f(p: Int) { }
f({ val y = 2; y * y })

leads to type error, Int != () -> Int

What about the first example with the block as the RHS of a val definition? Is that rejected as well in Kotlin?

EDIT: I see that’s forbidden as well. Presumably you have to write

val x = run {
  val y = 2
  y * y
}

It is not rejected, but in Scala the type of x would be Int, in Kotlin it would be () -> Int

Here are the examples in Kotlin Fiddle: Kotlin Playground: Edit, Run, Share Kotlin Code Online

Edit:

Maybe to add to that, { } is, it is just the Lambda Syntax in Kotlin: https://kotlinlang.org/docs/lambdas.html#lambda-expression-syntax

And the trailing lambda is just syntax sugar (I think :slight_smile: ) if the last parameter of a method is a function: https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas.

So it is a good solution for some specific use cases but does not generalize well to multiple function parameters:

For example with groupBy (takes two function parameters):

listOfTuples
    .groupBy({ it.first }) {it.second}  // with trailing lambda
    // .groupBy { it.first } {it.second} // not possible, does not compile

Note it is the same a _ in Scala

Note: In the above case I actually fallback to not using trailing lambdas at all: .groupBy({ it.first }, {it.second})

There is a subtle difference. In Scala { _ + _ } is equivalent to { (a, b) => a + b }; while in Kotlin { it + it } is { a -> a + a }.

And while we are at _ and it, there is one additional benefit of braces marking the lambda boundaries. In Scala, it happened to me that there were cases like a(b(_, _ + c), d(_)) when I needed to stop and think which _ was a parameter to which lambda. In kotlin it is always a parameter to the lambda defined by the nearest enclosing pair of braces which is really intuitive. And also using it in nested lambdas raises a warning.