Kotlin is `fun` - lambdas with receivers

Kotlin is `fun` - lambdas with receivers

As we already covered, Kotlin provides the ability to extend a class or an interface with new functionality without having to inherit from the class or use design patterns such as Decorator ⮕ Kotlin is fun - extension functions.

Kotlin also treats functions as first-class citizens, meaning they can be passed around. It also, of course, supports generics ⮕ Kotlin is fun - Function types, lambdas, and higher-order functions. (for more, have a look at the Series)

This means we can write something like this:

fun <T> T.doWith(op: (T) -> Unit) {
    op(this)
}

This extension function can be called on anything and will simply pass this anything as an argument to the lambda passed as a parameter. For example:

val someList = listOf(1, 2, 3, 4)

someList.doWith ({ list -> 
    println(list.size)
    println(list.sum())
})

This syntax is not following the conventions. Let's rewrite it in proper Kotlin, by removing parenthesis and using the implicit it parameter:

someList.doWith { // it -> List<Int>
    println(it.size)
    println(it.sum())
}

This is better. Notice though that the it is redundant. We would rather be able to write:

someList.doWith { // this -> List<Int>
  println(size)
  println(sum())
}

But how? Read on!

Lambdas with receivers

To make this magic happen, the only necessity is to change the type of the op argument from (T) -> Unit to T.() -> Unit. This special syntax, A.(B), denotes a receiver object:

Inside the body of the function literal, the receiver object passed to a call becomes an implicit this, so that you can access the members of that receiver object without any additional qualifiers, or access the receiver object using a this expression.

The final implementation is now:

fun <T> T.doWith(op: T.() -> Unit) {
    op(this)
}

listOf(1, 2, 3, 4).doWith {
    println(size)
    println(this.sum()) // "this" is optional 
}

It is just a bit of sugar-coating, but admit this syntax looks cool 😎.


Side note

In the real world, we would also want this doWith function to be inline, to improve performances:

inline fun <T> T.doWith(op: T.() -> Unit) {
    op(this)
}

For more information on receivers (and more examples), see the docs on Function literals with receivers.