The provided content offers an in-depth explanation of functions with receiver in Kotlin, differentiating them from extension functions, and detailing their syntax, usage, and equivalence to regular functions.
Abstract
The article delves into the concept of functions with receiver in Kotlin, emphasizing their distinction from extension functions. It clarifies that while both are related to the idea of extending functionality, functions with receiver are expressions that can be assigned and passed around, whereas extension functions are statements that cannot. The content explores the syntax for defining function types with receiver, function literals with receiver, and how to instantiate them using method references, anonymous functions, and lambdas. It also discusses the interchangeability of function types with and without receivers, the compilation of extension functions into regular functions with the receiver as the first argument, and the two ways to invoke functions with receiver. The article concludes by illustrating how functions with receiver can be invoked with an implicit receiver when in scope, a concept that is crucial for understanding Kotlin's scope functions.
Opinions
The author acknowledges that the distinction between functions with receiver and extension functions can be confusing but asserts that for most practical purposes, the difference is negligible.
The article suggests that understanding the syntactic sugar aspect of extension functions is key to grasping their true nature, as they compile down to regular functions with the receiver as an additional parameter.
The author expresses that functions with receiver are among the most powerful and expressive features of Kotlin when used correctly, hinting at their potential to enhance code readability and maintainability.
The content is part of a larger series, "The Kotlin Primer," which is intended to facilitate Kotlin adoption within Java-centric organizations, indicating the author's belief in Kotlin's value proposition for such environments.
The author provides Kotlin Playground examples to concretely demonstrate the concepts discussed, showing a preference for interactive learning and practical application over theoretical explanation alone.
Functions with Receiver
An in-depth explanation of functions with receiver, how they differ from extension functions, how they are represented and how to invoke them
— — — — — — — — — — — — — — —
THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.
This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.
Since Kotlin has function types and function literals, it is natural to ask if the concept of extension functions has an equivalent counterpart among them. It turns out that Kotlin has both function types with receiver and function literals with receiver. The result of instantiating a function type with receiver is called a function with receiver. These can be among the most powerful and expressive features Kotlin has to offer, when used correctly. In this lesson, we will only talk about syntax, and we will leave applications for a separate lesson.
Before we start, it is important to realize that functions with receiver are not the same thing as extension functions. Functions with receiver are expressions, extension functions are statements. The former can be assigned and passed around, the other cannot (only references to extension functions can). A function with receiver is created by instantiating a function type with receiver, while an extension function is created by an extension function declaration.
I’ll admit that this is a little confusing, even for me, but honestly, for the most part, you can just forget there’s a difference. The reason you need to distinguish between the two at all has to do with how each of them is invoked, which we’ll talk about in a few paragraphs.
Types
Functions with receivers have their own types, which follow fairly naturally from the way normal function types are written. The type of a function accepting a T and producing an R is denoted (T) -> R. The type of a function with receiver S, accepting a T and producing and R is denoted S.(T) -> R.
Literals
A function type with receiver can be instantiated by:
There are two types of function literals with receiver:
An anonymous function with receiver
A lambda with receiver
Functions With Receiver
The result of instantiating a function type with receiver is called a function with receiver, in the same way that the result of instantiating a function type is called a function.
Let’s give some examples:
Using references to methods, extension functions and bound callable references
Using an anonymous function with receiver
This syntax is not very common, since lambdas are almost always preferred over anonymous functions:
Using a lambda with receiver
This is the most common form of function literals with receiver you will encounter:
Equivalence Between Functions With and Without Receiver
As we’ve said before, one of the key things to understand about extension functions, and therefore functions with receiver, is that they are only syntactic sugar. In reality, they get compiled to regular functions with the receiver as the first argument.
It is for this reason that the types T.() -> S and (T) -> S are completely interchangeable — a value of one type can be assigned (or safely cast) to the other.
This is also important to understand when calling extensions from Java code — you just pass the receiver as the first parameter.
Invoking Functions With Receiver
Because functions with receiver are interchangeable with functions without receiver, there are two ways to invoke them:
prepending it with the receiver (basically the same syntax as calling an extension function)
passing the receiver as the first argument
This is where we finally see the difference between extension functions and functions with receiver:
Extensions functions can only be invoked using the receiver syntax, i.e. three.sum(3) or 3.sum(3)
Invoking a Function With Receiver With a Receiver In Scope
While this is a natural consequence of what we just went through, I feel it warrants a special mention.
As you can see, someFun is an extension function, which means that when you want to call it, you need to specify a receiver. However, notice that there is no need to specify the receiver explicitly when calling it from inside A, because there already is a receiver in scope — this — and it is of the same type as the receiver of someFun. Due to this, the receiver is implicit.
It’s the same with methods:
Now, that might seem pretty obvious, but the following example shows a similar situation that might not be as evident when you first encounter it:
We can see that block is a function with receiver A, which means it needs to be called on an instance of A. However, we can also see that runOn is an extension function defined on the same type as the receiver of block. This means that there is a A receiver available — this — and writing block() is the same as if you wrote this.block() — the receiver is specified implicitly. It's exactly the same principle as the previous example with methods, only with extension functions.
Here’s a very similar but slightly different version of runOn:
The difference is that verySimilarButSlightlyDifferentRunOn is no longer an extension function, so there is no receiver in scope. Instead, the receiver is passed as the first parameter, and therefore must be specified explicitly when calling block.
Since block is a function with receiver, we could also do this:
This runOn function is pretty useless - all it does is run the supplied function and returns the result. However, if you add generics to the mix, you get a pretty useful utility that we'll talk about more in the lesson on scope functions.
For illustration purposes, here’s how you would call runOn and verySimilarButSlightlyDifferentRunOn: