With the receiver in scope
Repetitio est mater studiorum (Latin, repetition is the mother of all learning).
Repetition is great for study, but a bane of software development. Repetitive code is boring and error-prone. Let’s look at a hypothetical example inspired by imperative UI frameworks that are still being used a lot nowadays, even though their heyday is past. We might find ourselves having to write code like this:
applicationWindow.title = "Just an example"
applicationWindow.position = FramePosition.AUTO
applicationWindow.content = createContent()
applicationWindow.show()
Referencing applicationWindow
object over and over again is very explicit but it is not pretty. We can make this code better using with
scope function from the Kotlin standard library:
with(applicationWindow) { // this: ApplicationWindow
title = "Just an example"
position = FramePosition.AUTO
content = createContent()
show()
}
This code now has more lines, but the number of lines is not the key metric you should be looking at when judging the code. The code looks cleaner. It groups all the initialization of the applicationWindow
object into a separate, syntactically distinct block. It directly represents the developer’s intent to perform all of these actions on applicationWindow
together.
The block of code inside of with(x) { ... }
is easily recognizable by any developer trained in a mainstream object-oriented language — it is similar to the code you encounter inside a method of the corresponding class where this
refers to the method’s receiver object and all unqualified references like title
refer to this object’s properties and methods.
In the Kotlin programming language writing a method is not the only way to get access to object members without having to repeat the object’s name. Kotlin has support for extension functions that allow us to write a method-like looking code outside of the class body:
fun ApplicationWindow.configure() { // this: ApplicationWindow
title = ... // no need to qualify title reference
}
The ability to write extension functions naturally extends into the ability to write lambdas with a receiver. Kotlin’s with
is not a keyword nor intrinsic in the language. It is a library function with a trivial implementation:
fun <T, R> with(receiver: T, block: T.() -> R): R =
receiver.block()
When you use it, the argument’s type becomes the receiver in the corresponding block
and IDE helpfully annotates the code inside the block with this: ApplicationWindow
comment.
Great feature! Why would not we use it everywhere? Providing access to the object reference makes our code concise. Concision is the chief Kotlin’s motto, it is the first one mentioned in “Why Kotlin” section on kotlinlang.org, isn’t it?
However, looking at the Kotlin standard library we will not find a lot of higher-order functions that are using parameters with receivers. For example, collection manipulation functions, like filter
, are declared this way:
fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
A predicate
has the type of a regular function (T) -> Boolean
, and not a function with the receiver T.() -> Boolean
. When we use filter
on some collection of domain-specific objects we might end up with code like this:
persons.filter { it.firstName == name }
Here it is perfectly clear that Person.firstName
property of each element in the persons
collection is being compared with the specific name
value.
If filter
was defined with predicate: T.() -> Boolean
parameter then the corresponding code would look like this:
persons.filter { firstName == name }
It is much less clear what is going on here at first sight. Is Person.firstName
being compared to name
here or isPerson.name
being compared to firstName
? We’ll need to look around to figure it out. The more code and more context we have around, the harder it will be to see.
The key quality of the code that we are looking to achieve is not concision, but readability. The goal is to make code clear and unambiguously understandable to a person who is reading the code. There is a fine balance between longer code, that has too much repetition, making it harder to grasp the essence of the code, and shorter code, that relies too much on the context, knowledge, and attention of the reader.
Don’t abuse this Kotlin language flexibility in your API design. Prefer to be explicit. Lambdas with receivers are a great tool to design clean-looking DSLs, where the structure of the code mimics the structure of the domain you are modeling. They allow you to provide a context in your lambdas with a carefully picked set of available extensions that you need. Do use them for DSLs and for type-safe builders.
For general-purpose needs rely on Kotlin scope functions with
, run
, apply
. Their use is idiomatic in Kotlin. However, resist the urge to use them just to make your code shorter. Make sure that their use is consistent with the intent of the code you are writing. Kotlin documentation has a helpful page with examples and the intended use for scope functions.