Skip to main content

Kotlin Receiver Lambdas

In Kotlin's extensive lambda functionality, receiver lambdas (also called function literals with receiver) are a powerful feature that might initially seem complex but opens up elegant possibilities for your code. This feature is the foundation for many of Kotlin's DSLs (Domain-Specific Languages) and enables expressive, readable code.

What Are Receiver Lambdas?

A receiver lambda is a special kind of lambda function that has a receiver object. Inside the lambda body, you can call methods and access properties of this receiver object without any qualifiers, as if the code were written inside the receiver object itself.

Think of a receiver lambda as saying: "Here's a block of code that should execute in the context of this specific object."

The general syntax looks like this:

kotlin
val receiverLambda: Receiver.() -> ReturnType = { 
// "this" refers to Receiver
// methods and properties of Receiver can be accessed directly
}

Basic Example of Receiver Lambdas

Let's start with a simple example:

kotlin
fun main() {
val greet: String.() -> Unit = {
println("Hello, $this!")
}

// Using the receiver lambda
"World".greet() // Output: Hello, World!
"Kotlin".greet() // Output: Hello, Kotlin!
}

In this example:

  • String.() -> Unit defines a lambda that has a String as its receiver and returns nothing (Unit)
  • Inside the lambda, this refers to the String object on which the lambda is called
  • We invoke the lambda on "World" and "Kotlin" as if it were a method of the String class

Understanding the Syntax

Let's break down the syntax:

  1. Receiver.() indicates that this lambda has a receiver of type Receiver
  2. -> ReturnType specifies what the lambda returns
  3. Inside the lambda, this refers to the receiver object (and can be omitted)
  4. Properties and methods of the receiver can be accessed directly

Defining and Using Functions with Receiver Lambdas

You can define functions that take receiver lambdas as parameters:

kotlin
fun String.customOperation(operation: String.() -> Unit) {
this.operation() // Apply the operation to the string
}

fun main() {
val message = "Hello"

message.customOperation {
println("Original: $this")
println("Length: ${this.length}")
println("Uppercase: ${toUpperCase()}") // Note: no 'this' needed here
}
}

Output:

Original: Hello
Length: 5
Uppercase: HELLO

Notice how inside the lambda, we access the string's properties and methods directly, without needing to use this explicitly.

Practical Use Case: Building a Simple HTML DSL

One of the most common applications of receiver lambdas is creating DSLs (Domain-Specific Languages). Let's create a simple HTML builder:

kotlin
class HTMLBuilder {
private val content = StringBuilder()

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

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

fun div(init: HTMLBuilder.() -> Unit) {
content.append("<div>")
init() // 'this' is the HTMLBuilder instance
content.append("</div>")
}

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

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

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

println(htmlContent)
}

Output:

<h1>Welcome to Kotlin</h1><p>This is a paragraph</p><div><p>This paragraph is inside a div</p></div>

This example demonstrates how receiver lambdas enable us to create expressive DSLs. The html function takes an HTMLBuilder.() -> Unit lambda, allowing us to call methods on the HTMLBuilder instance without explicitly referring to it.

Type-Safe Builders with Receiver Lambdas

Kotlin's type-safe builders leverage receiver lambdas to create structured, readable code. Here's another example for building a menu:

kotlin
class MenuItem(val name: String, val price: Double)

class Menu {
private val items = mutableListOf<MenuItem>()

fun item(name: String, price: Double) {
items.add(MenuItem(name, price))
}

fun section(name: String, init: Menu.() -> Unit) {
println("=== $name ===")
init() // Apply the lambda to this Menu instance
}

fun printMenu() {
items.forEach { println("${it.name}: $${it.price}") }
}
}

fun createMenu(init: Menu.() -> Unit): Menu {
val menu = Menu()
menu.init()
return menu
}

fun main() {
val lunchMenu = createMenu {
section("Appetizers") {
item("Garlic Bread", 3.99)
item("Soup of the Day", 4.50)
}

section("Main Course") {
item("Spaghetti", 11.99)
item("Steak", 15.99)
item("Salmon", 14.50)
}
}

lunchMenu.printMenu()
}

Output:

=== Appetizers ===
=== Main Course ===
Garlic Bread: $3.99
Soup of the Day: $4.5
Spaghetti: $11.99
Steak: $15.99
Salmon: $14.5

Using with Standard Library Functions

Kotlin's standard library contains several functions that use receiver lambdas:

The apply function

kotlin
fun main() {
val person = Person().apply {
name = "John"
age = 30
email = "[email protected]"
}

println(person) // Person(name=John, age=30, [email protected])
}

data class Person(var name: String = "", var age: Int = 0, var email: String = "")

The with function

kotlin
fun main() {
val person = Person("Alice", 25, "[email protected]")

val description = with(person) {
"Name: $name, Age: $age, Contact: $email"
}

println(description) // Name: Alice, Age: 25, Contact: [email protected]
}

When to Use Receiver Lambdas

Receiver lambdas are particularly useful when:

  1. Building DSLs where you want a fluent, readable API
  2. Working with context-specific operations where a certain object is the primary focus
  3. Extending functionality of classes without modifying them directly
  4. Creating builder patterns for complex object construction

Comparing Regular Lambdas vs. Receiver Lambdas

To understand the difference, let's compare:

kotlin
// Regular lambda
val regular: (StringBuilder) -> Unit = { sb ->
sb.append("Hello ")
sb.append("World")
}

// Receiver lambda
val withReceiver: StringBuilder.() -> Unit = {
append("Hello ") // No need to qualify with 'this'
append("World")
}

fun main() {
val sb1 = StringBuilder()
regular(sb1) // Pass the StringBuilder as a parameter
println(sb1.toString()) // Output: Hello World

val sb2 = StringBuilder()
sb2.withReceiver() // Call the lambda on the StringBuilder
println(sb2.toString()) // Output: Hello World
}

The key differences are:

  • Regular lambdas receive their context as parameters
  • Receiver lambdas operate directly within the context of their receiver
  • Receiver lambdas make for more concise code when working heavily with a specific object

Summary

Receiver lambdas in Kotlin are powerful constructs that allow you to write code as if it were within the context of a specific object. This feature:

  • Enables the creation of expressive, readable DSLs
  • Makes context-specific operations more concise
  • Forms the foundation for many of Kotlin's standard library functions like apply, with, run, etc.
  • Allows for elegant extension of functionality without direct class modification

By understanding and using receiver lambdas, you can write more expressive, concise, and readable Kotlin code, especially when working with complex structures or specific contexts.

Exercises

  1. Create a simple DSL for building a shopping list with categories
  2. Implement a function that uses a receiver lambda to apply formatting to a string (like adding bold, italics, etc.)
  3. Extend the HTML DSL above to include more HTML tags and attributes
  4. Create a custom scope function similar to apply or with using receiver lambdas

Additional Resources

Happy coding with Kotlin receiver lambdas!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)