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:
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:
// 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
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
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
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:
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:
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:
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:
- Writing code in the context of a specific object
- Accessing properties and methods of that object directly
- Creating expressive DSLs with readable, concise code
- 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
- Create a simple DSL for building a to-do list with tasks and subtasks.
- Extend the HTML DSL with more HTML elements like
ul
,li
, anda
with attributes. - Build a DSL for creating test data for your application.
- 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! :)