As a newcomer to Kotlin, I stumbled upon infix functions quite unexpectedly. They don’t immediately look like Python’s Magic Methods or C++’s operator overloading.

At first I thought they were reserved words when I saw them in code like:

val medicines = mapOf(
    "rosuvastatin" to "bad cholesterol",
    "losartan" to "high blood pressure",
    "semaglutide" to "type 2 diabetes"
)

In Kotlin, an Infix Function allows us to call a method without using dots . or parentheses ().

Instead of writing "rosuvastatin".to("bad cholesterol"), Kotlin lets us write "rosuvastatin" to "bad cholesterol".

They build on top of another feature of the language: Extension functions.

Kotlin sets three requirements for infix functions:

  1. It must be declared with the infix keyword.
  2. It must be a member function or an extension function (so it has a left-hand side this).
  3. It must take exactly one parameter (the right-hand side argument).

Example 1: How to is actually implemented in Kotlin

Here is the exact implementation of to from the Kotlin Standard Library:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

When we write "rosuvastatin" to "bad cholesterol", Kotlin translates this to "rosuvastatin" and that to "bad cholesterol", returning Pair("rosuvastatin", "bad cholesterol").

Example 2: Building our own domain DSL

Imagine we are building a financial domain model. We can write custom infix functions to make domain logic read like plain English.

We start by introducing our Money model:

data class Money(var amount: Double, val currency: String) {
    override fun toString() = "$amount $currency"

    operator fun minusAssign(other: Money) {
        if (this.currency != other.currency) {
            throw IllegalArgumentException(
                "It is a ${this.currency} wallet; " +
                "it cannot debit in ${other.currency}."
            )
        }
        this.amount -= other.amount
    }

    operator fun minusAssign(rawValue: Double) {
        this.amount -= rawValue
    }
}

It overrides the minusAssign operator to permit subtracting a raw numeric amount or using another Money, in which case it checks whether the currency is correct and throws an exception otherwise.

We proceed by creating an extension function for Double which converts an instance of it to Money:

infix fun Double.of(currency: String): Money {
    return Money(this, currency)
}

The Wallet class permits holding an amount of Money for a user:

data class Wallet(val userId: String, var balance: Money) {
    infix fun debit(amount: Double) {
        this.balance -= amount
    }

    infix fun debit(amount: Money) {
        this.balance -= amount
    }
}

The Wallet class has two infix functions to allow debiting using a raw amount (Double) or a Money amount.

Finally, we put it all together and analyze how it works:

fun main() {
    // Create an instance of `Money` from the infix function
    // added as extension for `Double`
    val balance = 500.0 of "GBP"

    println(balance)
    // 500.0 GBP

    // Create a `Wallet` for user `USER-1` with the balance
    // created above.
    val userWallet = Wallet(userId = "USER-1", balance = balance)

    println("Initial balance: ${userWallet.balance}")
    // Initial balance: 500.0 GBP

    // Uses the `debit` infix function to debit 150.0 raw amount
    // from the user's wallet
    userWallet debit 150.0

    println("Intermediate balance: ${userWallet.balance}")
    // Intermediate balance: 350.0 GBP

    // Composes the creation of a `Money` amount with the `of`
    // infix function from `Double` and the debit from the user's
    // wallet using the `debit` infix function. Notice the use
    // of parentheses to indicate the intended order of operations.
    userWallet debit (100.0 of "GBP")

    println("Final balance: ${userWallet.balance}")
    // Final balance: 250.0 GBP

    try {
        // Here we try to debit an amount with a `Money` instance
        // created for a different currency, triggering an exception.
        userWallet debit (150.0 of "USD")
    } catch (e: Exception) {
        println("Error: ${e.message}")
        // Error: It is a GBP wallet; it cannot debit in USD.
    }
}

Example 3: Writing expressive unit tests

Moving on with the trend initiated in the previous section, we can create unit tests that also read like plain English.

We create an infix function that permits checking equality for any generic type T:

infix fun <T> T.shouldBeEqualTo(expected: T) {
    if (this != expected) {
        throw AssertionError("Expected $expected but got $this")
    }
}

And we make use of it in a test function:

class MainTest {
    @Test
    fun `money can be debited from the wallet`() {
        // Arrange
        val myWallet = Wallet("USER-1", (300.0 of "GBP"))

        // Act
        myWallet debit 150.0

        // Assert
        myWallet.balance shouldBeEqualTo (150.0 of "GBP")
    }
}

Depending on our coding style preferences we could even do something like:

infix fun <T> T.`should be equal to`(expected: T) {
    if (this != expected) {
        throw AssertionError("Expected $expected but got $this")
    }
}

class MainTest {
    @Test
    fun `money can be debited form the wallet`() {
        // Arrange
        val myWallet = Wallet("USER-1", (300.0 of "GBP"))

        // Act
        myWallet debit 150.0

        // Assert
        myWallet.balance `should be equal to` (150.0 of "GBP")
    }
}

However, the excessive use of backticks bloats the intent a little bit.

Summary

Infix functions work pretty much as syntactic sugar and might look a little bit misleading for the distracted, but once we understand them, they become a powerful resource for improving our code’s readability.

Standard Method Call Infix Syntax Requirement
1.to("USD") 1 to "USD" infix fun <A,B> A.to(that: B)
wallet.debit(50.0) wallet debit 50.0 infix fun Wallet.debit(amount: Double)
50.0.of("EUR") 50.0 of "EUR" infix fun Double.of(currency: String)