Skip to main content

Kotlin Receiver Functions

Introduction

Receiver functions are a powerful feature in Kotlin that allow you to call methods and access properties as if you were inside an object of a specific type. This concept is fundamental to building Domain Specific Languages (DSLs) in Kotlin, enabling expressive, readable, and concise code.

In this tutorial, we'll explore receiver functions, understand their syntax, and see how they form the building blocks for Kotlin DSLs.

What Are Receiver Functions?

A receiver function is a function literal (lambda) that can be called with a specified receiver object. The receiver object becomes this within the body of the function, allowing direct access to its methods and properties without explicit qualification.

Think of it as temporarily "extending" an object with additional functionality or executing a block of code in the context of that object.

Basic Syntax

Let's start with the basic syntax of a receiver function:

kotlin
val receiverFunction: Type.() -> ReturnType = {
// 'this' refers to the receiver object of Type
// You can access Type's properties and methods directly
}

The .() notation in the type declaration indicates that this function has a receiver of the specified type.

Extension Functions vs. Receiver Functions

You might be familiar with Kotlin's extension functions. Receiver functions are closely related but used differently:

  • Extension Functions: Define new functions for existing classes
  • Receiver Functions: Function literals (lambdas) that execute with a specific receiver

Let's see a simple comparison:

kotlin
// Extension function
fun String.addExclamation(): String = "$this!"

// Receiver function (lambda with receiver)
val addExclamation: String.() -> String = { "$this!" }

fun main() {
// Using the extension function
println("Hello".addExclamation()) // Output: Hello!

// Using the receiver function
println("Hello".addExclamation()) // Output: Hello!
}

Using with, apply, run, and other Scope Functions

Kotlin's standard library includes several scope functions that utilize receiver functions:

The with Function

kotlin
fun main() {
val person = Person("John", 30)

// Without 'with'
println(person.name)
println(person.age)
person.increaseAge()

// With 'with'
with(person) {
println(name) // Access property directly
println(age) // Access property directly
increaseAge() // Call method directly
println("$name is now $age years old")
}
}

class Person(val name: String, var age: Int) {
fun increaseAge() {
age++
}
}

Output:

John
30
John
30
John is now 31 years old

The apply Function

kotlin
fun main() {
val person = Person("John", 30).apply {
age = 31 // Modify property
celebrateBirthday() // Call method
}

println("${person.name} is ${person.age} years old")
}

class Person(val name: String, var age: Int) {
fun celebrateBirthday() {
println("Happy Birthday!")
age++
}
}

Output:

Happy Birthday!
John is 32 years old

The run Function

kotlin
fun main() {
val person = Person("John", 30)

val greeting = person.run {
// 'this' is the person object
"Hello, my name is $name and I am $age years old"
}

println(greeting)
}

class Person(val name: String, val age: Int)

Output:

Hello, my name is John and I am 30 years old

Creating Your Own Receiver Functions

Now, let's create our own functions that accept receiver functions as parameters:

kotlin
fun StringBuilder.buildString(action: StringBuilder.() -> Unit): String {
action() // 'this' is the StringBuilder instance
return toString()
}

fun main() {
val result = StringBuilder().buildString {
append("Hello, ")
append("World!")
append("\n")
append("This is a DSL example.")
}

println(result)
}

Output:

Hello, World!
This is a DSL example.

Building a Simple DSL with Receiver Functions

Let's create a simple HTML DSL to demonstrate how receiver functions enable the creation of DSLs:

kotlin
class HTMLBuilder {
private val content = StringBuilder()

fun h1(text: String) {
content.append("<h1>$text</h1>\n")
}

fun p(text: String) {
content.append("<p>$text</p>\n")
}

fun div(block: HTMLBuilder.() -> Unit) {
content.append("<div>\n")
this.block()
content.append("</div>\n")
}

override fun toString() = content.toString()
}

fun html(block: HTMLBuilder.() -> Unit): String {
val builder = HTMLBuilder()
builder.block()
return builder.toString()
}

fun main() {
val htmlContent = html {
h1("Welcome to Kotlin DSL")
p("This is a paragraph")
div {
p("This paragraph is inside a div")
h1("A heading inside the div")
}
}

println(htmlContent)
}

Output:

<h1>Welcome to Kotlin DSL</h1>
<p>This is a paragraph</p>
<div>
<p>This paragraph is inside a div</p>
<h1>A heading inside the div</h1>
</div>

Real-World Application: Configuration DSL

Receiver functions are perfect for creating configuration DSLs. Here's a simple example for a database connection:

kotlin
class DatabaseConfig {
var host: String = "localhost"
var port: Int = 5432
var username: String = ""
var password: String = ""
var database: String = ""

fun credentials(setup: Credentials.() -> Unit) {
val credentials = Credentials()
credentials.setup()
this.username = credentials.username
this.password = credentials.password
}

override fun toString(): String {
return "DatabaseConfig(host='$host', port=$port, username='$username', password='${password.map { '*' }.joinToString("")}', database='$database')"
}
}

class Credentials {
var username: String = ""
var password: String = ""
}

fun database(setup: DatabaseConfig.() -> Unit): DatabaseConfig {
val config = DatabaseConfig()
config.setup()
return config
}

fun main() {
val dbConfig = database {
host = "db.example.com"
port = 3306
database = "users"

credentials {
username = "admin"
password = "secret123"
}
}

println("Database configuration: $dbConfig")

// In a real application, you would use the configuration:
// val connection = connectToDatabase(dbConfig)
}

Output:

Database configuration: DatabaseConfig(host='db.example.com', port=3306, username='admin', password='*********', database='users')

Summary

Receiver functions are a powerful feature in Kotlin that enable:

  1. Writing code in the context of a specific object
  2. Accessing properties and methods of that object directly
  3. Creating expressive DSLs with readable, concise code
  4. Building configuration APIs with a natural syntax

They form the foundation of Kotlin's DSL capabilities and are used extensively in frameworks like Gradle Kotlin DSL, Ktor, and more.

By understanding receiver functions, you've made a significant step toward mastering Kotlin DSLs and can now create more expressive, readable APIs in your own applications.

Exercises

  1. Create a simple DSL for building a to-do list with tasks and subtasks.
  2. Extend the HTML DSL with more HTML elements like ul, li, and a with attributes.
  3. Build a DSL for creating test data for your application.
  4. Create a DSL for setting up a network request with headers, query parameters, and body.

Additional Resources



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)