How Does Kotlin Lambda Receiver Work as Java Consumer?

The assertj library has the java function

 public static void assertSoftly(Consumer<SoftAssertions> softly) {
    SoftAssertionsProvider.assertSoftly(SoftAssertions.class, softly);
  }

I noticed accidentally that you can pass a lambda receiver function into this and everything just works…

fun assertSoftly(block: SoftAssertions.() -> Unit) {
  SoftAssertions.assertSoftly(block) // compiles and works
}

This makes for a much nicer kotlin-friendly experience

assertSoftly {
  assertThat("a").isEqualTo("b") // no need to prefix assertThat with 'it', as we would need to if we were using a Consumer
}

Technically I would expect only something like the below to work

fun assertSoftly(block: (SoftAssertions) -> Unit) {
  SoftAssertions.assertSoftly(block) // also compiles and works
}

I tried looking up where the first behavior would be explained (lambda receiver could be used as consumer) but couldn’t find anything. Is this behavior explained anywhere?

Thanks

1 Like

SoftAssertions.() -> Unit and (SoftAssertions) -> Unit are pretty much the same types. The only difference is how they expose their params inside their own source code, but from the outside and after compiling they are probably exactly the same.

We can also freely convert between them:

val a: String.() -> Unit = {}
val b: (String) -> Unit = a
val c: String.() -> Unit = b
1 Like

Non-literal values of function types with and without a receiver are interchangeable, so the receiver can stand in for the first parameter, and vice versa. For instance, a value of type (A, B) -> C can be passed or assigned where a value of type A.(B) -> C is expected, and the other way around:

2 Likes

Ah, that makes sense…thanks for the link!

Extension functions are just syntactic sugar anyway, where the receiver becomes an implicit first parameter.

1 Like