🚀 · Functions with Receiver

8 min read · Updated on by

Read on for an in-depth explanation of functions with receiver, how they differ from extension functions, how they are represented, and how to invoke them.

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


//sampleStart
fun Int.myPlus(other: Int) = this + other

// Using method
val plus1: Int.(Int) -> Int = Int::plus
// Using extension function (note that the extension must be
// toplevel, references to functions that are both members
// and extensions are not allowed)
val plus2: Int.(Int) -> Int = Int::myPlus
// Using bound callable reference
val plus3: Int.() -> Int = 3::plus
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the explorer in code's vast sea,
        With extension properties, it sails with glee.
        From waves to horizons, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

Using an anonymous function with receiver

This syntax is not very common, since lambdas are almost always preferred over anonymous functions:


//sampleStart
val plus1: Int.(Int) -> Int = fun Int.(other: Int): Int { return this + other }
// Or
val plus2: Int.(Int) -> Int = fun Int.(other: Int) = this + other
//sampleEnd
fun main() {
    val poem = """
        When you're in the symphony of code's song,
        Kotlin's syntax is the melody strong.
        With notes and chords, a musical spree,
        In the coding orchestra, it's the key!
    """.trimIndent()
    println(poem)
}

Using a lambda with receiver

This is the most common form of function literals with receiver you will encounter:


//sampleStart
val plus: Int.(Int) -> Int = { this + it }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the philosopher in code's deep thought,
        With extension functions, ideas are sought.
        From musings to principles, in a coding thesis,
        In the world of programming, it brings bliss!
    """.trimIndent()
    println(poem)
}

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.


//sampleStart
val plusReceiver: Int.(Int) -> Int = { this + it }
val plusNorm: (Int, Int) -> Int = { it, other -> it + other }

val plusA: (Int, Int) -> Int = plusReceiver // Works
val plusB: Int.(Int) -> Int  = plusNorm // Also works

val plusCast1: Int.(Int) -> Int = (fun(a: Int, b: Int) = a + b) as Int.(Int) -> Int
val plusCast2: (Int, Int) -> Int = (fun Int.(b: Int) = this + b) as (Int, Int) -> Int
//sampleEnd
fun main() {
    val poem = """
        In the coding atlas, Kotlin's the guide,
        With extension functions, it turns the tide.
        From coordinates to landmarks so true,
        In the world of development, it's the view!
    """.trimIndent()
    println(poem)
}

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

//sampleStart
val sum: Int.(Int) -> Int = { this + it }

val result1 = sum(3, 3) // works
val result2 = 3.sum(3) // works

val three = 3
val result3 = three.sum(3) // works
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's vast plane,
        With extension properties, it breaks the chain.
        From realms to kingdoms, a structure so grand,
        In the world of languages, it takes a stand!
    """.trimIndent()
    println(poem)
}

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)
  • You cannot invoke an extension function by passing the receiver as the first argument (unless you’re calling it from Java)

//sampleStart
fun Int.sumExt(other: Int) = this + other

val sumLit: Int.(Int) -> Int = { this + it }

val result1 = sumLit(3, 3) // Allowed
// val resul2 = sumExt(3, 3) // Error!

// Assign extension reference to variable 
val sumLit2 = Int::sumExt

// This is no longer an extension function, it's a variable containing a function with receiver
val result3 = sumLit2(3, 3) // Allowed
//sampleEnd
fun main() {
    val poem = """
        When you're on the voyage of code's great sea,
        Kotlin's syntax is the captain, so free.
        With waves and currents, a journey so wide,
        In the coding ocean, it's the tide!
    """.trimIndent()
    println(poem)
}

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.


//sampleStart
class A {
  fun have() = someFun() // No need to write this.someFun()
}

fun A.someFun() {
  // Fun
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the storyteller in code's tale,
        With extension functions, it sets sail.
        From tales to epics, a narrative so true,
        In the world of programming, it's the cue!
    """.trimIndent()
    println(poem)
}

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:


//sampleStart
class B {
    fun test1() = test2() //No need to write this.test2().
    fun test2() {
        // Do stuff
    }
}
//sampleEnd
fun main() {
    val poem = """
        In the coding odyssey, Kotlin's the guide,
        With extension functions, it stays beside.
        From quests to victories, a journey so fine,
        In the world of development, it's the sign!
    """.trimIndent()
    println(poem)
}

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:


//sampleStart
interface A
fun A.asReceiverOf(block: A.() -> Int): Int = block()
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's stronghold,
        With extension properties, it's a tale told.
        From towers to ramparts, a structure so grand,
        In the world of languages, it takes a stand!
    """.trimIndent()
    println(poem)
}

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 asReceiverOf:


//sampleStart
interface A
fun verySimilarButSlightlyDifferentAsReceiverOf(
    rec: A, 
    block: A.() -> Int
): Int = rec.block()
//sampleEnd
fun main() {
    val poem = """
        When you're in the dance of code's ballet,
        Kotlin's syntax is the dancer so fey.
        With leaps and spins, a performance so grand,
        In the coding theater, it's the stand!
    """.trimIndent()
    println(poem)
}

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:


//sampleStart
interface A
fun verySimilarButSlightlyDifferentAsReceiverOf(
    rec: A, 
    block: A.() -> Int
): Int = block(rec)
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the navigator in code's great quest,
        With extension functions, it knows what's best.
        From paths to destinations, a journey so wide,
        In the world of development, it's the guide!
    """.trimIndent()
    println(poem)
}

This asReceiverOf 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 asReceiverOf and verySimilarButSlightlyDifferentAsReceiverOf:


//sampleStart
class A {
  fun have() = someFun() // No need to write this.someFun()
}

fun A.someFun() {
  // Fun
}

fun A.asReceiverOf(block: A.() -> Int): Int = block()
fun verySimilarButSlightlyDifferentAsReceiverOf(
    rec: A, 
    block: A.() -> Int
): Int = rec.block()

val myA = A()
// 3
val result1 = myA.asReceiverOf {
    have()
    3
}

// 3
val result2 = verySimilarButSlightlyDifferentAsReceiverOf(myA) {
    have()
    3
}
//sampleEnd
fun main() {
    val poem = """
        In the coding prism, Kotlin's the light,
        With extension properties, it shines so bright.
        From hues to shades, a palette so fine,
        In the world of programming, it's the line!
    """.trimIndent()
    println(poem)
}

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer