
I remember the first time I saw this line of Kotlin code:
val coordinates = 10 to 20
Coming from Java, I thought “to” was some special keyword. Turns out, it’s just a regular function call! This is the magic of infix functions — one of Kotlin’s most elegant features that makes code read like natural language.
Today, I want to share everything I’ve learned about infix functions, including how they work under the hood and how you can create powerful, readable APIs with them.
What Are Infix Functions?
Infix functions allow some functions to be called without using the period and brackets. Instead of writing object.method(parameter), you can write object method parameter. It’s syntactic sugar that makes your code more readable and expressive.
Here’s how a normal function call looks vs an infix call:
// Normal function call
val pair1 = "key".to("value")
// Infix function call
val pair2 = "key" to "value"
Both do exactly the same thing, but the infix version reads more naturally.
The Rules: What Makes a Function “Infix-able”?
For a function to work as an infix function, it must follow three simple rules:
- It must be a member function or an extension function
- It must have exactly one parameter
- It must be marked with the
infix keyword
That’s it! Let me show you with examples:
// Member function
class Calculator(val value: Int) {
infix fun add(value: Int): Int {
return this.value + value
}
}
// Extension function
infix fun Int.powerOf(exponent: Int): Double {
return this.toDouble().pow(exponent)
}
fun main() {
// Usage
val calc = Calculator(5)
val result1 = calc add 10 // Member function
val result2 = 2 powerOf 3 // Extension function (result: 8.0)
}
Range Operators: Infix Functions You Use Every Day
Remember those range operators I mentioned at the start? rangeTo, downTo, until, step – they’re all infix functions! Let me show you what’s really happening:
The .. operator (rangeTo)
// What you write
for (i in 1..5) { /* ... */ }
// What actually happens
for (i in 1.rangeTo(5)) { /* ... */ }
The rangeTo function is defined as an infix function in the Int class.
The downTo operator
// Infix style
for (i in 10 downTo 1) { /* ... */ }
// Regular function call equivalent
for (i in 10.downTo(1)) { /* ... */ }
The until operator
// Infix style
for (i in 0 until 10) { /* ... */ }
// Regular function call
for (i in 0.until(10)) { /* ... */ }
The step function
// Infix chaining
for (i in 1..10 step 2) { /* ... */ }
// What actually happens
for (i in (1..10).step(2)) { /* ... */ }
Pretty cool, right? These operators that feel built into the language are just regular functions using infix notation!
How Infix Functions Work Under the Hood
When Kotlin compiles to Java bytecode, infix functions are converted to regular method calls. Here’s what happens:
// Kotlin infix call val result = 5 add 3 // Compiles to something like this Java code: // int result = calculator.add(3);
The Kotlin compiler automatically transforms the infix syntax into standard method calls. There’s no runtime overhead — it’s purely a compile-time transformation.
Let me show you with a practical example:
class MathOperations(private val value: Int) {
infix fun plus(other: Int) = MathOperations(value + other)
infix fun times(other: Int) = MathOperations(value * other)
override fun toString() = value.toString()
}
fun main() {
val math = MathOperations(5)
val result = math plus 3 times 2
}
This gets compiled to regular Java method calls behind the scenes, maintaining all the performance of normal function calls.
Real-World Use Cases: Where Infix Functions Shine
1. Domain-Specific Languages (DSLs)
Infix functions are perfect for creating readable DSLs. Here’s a simple test framework:
class TestAssertion<T>(private val actual: T) {
infix fun shouldBe(expected: T) {
if (actual != expected) {
throw AssertionError("Expected $expected but was $actual")
}
}
infix fun shouldNotBe(expected: T) {
if (actual == expected) {
throw AssertionError("Expected not to be $expected")
}
}
}
infix fun <T> T.should(assertion: TestAssertion<T>.() -> Unit) {
TestAssertion(this).assertion()
}
fun main() {
// Usage - reads like natural language!
val name = TestAssertion("John")
name shouldBe "John"
name shouldNotBe "Jane"
val age = TestAssertion(25)
age shouldBe 25
"john" should {
shouldBe("john")
shouldNotBe("doe")
}
10 should {
shouldBe(10)
shouldNotBe(5)
}
}
2. Configuration and Setup
class DatabaseConfig {
var host: String = ""
var port: Int = 0
var database: String = ""
}
infix fun DatabaseConfig.host(value: String) = apply { host = value }
infix fun DatabaseConfig.port(value: Int) = apply { port = value }
infix fun DatabaseConfig.database(value: String) = apply { database = value }
fun main() {
// Usage
val config = DatabaseConfig()
.host("localhost")
.port(5432)
.database("myapp")
// Or even more natural:
fun configureDatabase() = DatabaseConfig().apply {
this host "localhost"
this port 5432
this database "myapp"
}
}
3. Time and Duration APIs
data class Duration(val amount: Long, val unit: TimeUnit)
infix fun Int.seconds(unit: String): Duration = Duration(this.toLong(), TimeUnit.SECONDS)
infix fun Int.minutes(unit: String): Duration = Duration(this.toLong(), TimeUnit.MINUTES)
infix fun Int.hours(unit: String): Duration = Duration(this.toLong(), TimeUnit.HOURS)
fun main() {
// Usage
val timeout = 30 seconds "timeout"
val cacheExpiry = 5 minutes "cache"
val sessionDuration = 2 hours "session"
}
4. Mathematical Operations
data class Vector2D(val x: Double, val y: Double) {
infix fun dot(other: Vector2D): Double = x * other.x + y * other.y
infix fun cross(other: Vector2D): Double = x * other.y - y * other.x
}
fun main() {
// Usage
val v1 = Vector2D(3.0, 4.0)
val v2 = Vector2D(1.0, 2.0)
val dotProduct = v1 dot v2 // 11.0
val crossProduct = v1 cross v2 // 2.0
}
5. String Matching and Validation
infix fun String.matches(pattern: String): Boolean = this.matches(pattern.toRegex())
infix fun String.contains(substring: String): Boolean = this.contains(substring, ignoreCase = true)
infix fun String.startsWith(prefix: String): Boolean = this.startsWith(prefix, ignoreCase = true)
// Usage in validation
fun validateEmail(email: String): Boolean {
return email matches "^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"
&& email contains "@"
&& !(email startsWith "@")
}
Creating Your Own Infix Function Library
Let’s build a practical example — a fluent assertion library for testing:
class Expect<T>(val actual: T) {
infix fun toBe(expected: T) = apply {
if (actual != expected) {
throw AssertionError("Expected '$expected' but was '$actual'")
}
}
infix fun <T : Comparable<T>> Expect<T>.toBeGreaterThan(expected: T) = apply {
if (actual <= expected) {
throw AssertionError("Expected '$actual' to be greater than '$expected'")
}
}
infix fun toContain(element: Any) = apply {
if (actual is Collection<*> && element !in actual) {
throw AssertionError("Expected $actual to contain $element")
}
}
}
infix fun <T> T.should(block: Expect<T>.() -> Unit) {
Expect(this).apply(block)
}
fun main() {// Usage
val numbers = listOf(1, 2, 3, 4, 5)
numbers should {
toContain(3)
}
val name = "John Doe"
name should {
toBe("John Doe")
}
val age = 25
age should {
toBeGreaterThan(18)
}
}
Performance Considerations
One thing I love about infix functions is that they have zero runtime cost. The Kotlin compiler transforms them into regular method calls, so there’s no performance penalty. The presence of the infix keyword allows us to write code that is cleaner to read and easier to understand without sacrificing performance.
Here’s what the compiler does:
// Your infix code val result = calculator add 5 // Compiler generates (simplified): val result = calculator.add(5)
Best Practices for Infix Functions
1. Use Them Sparingly
Not every function should be infix. Save it for cases where it genuinely improves readability:
// Good - reads naturally user hasPermission "ADMIN" // Bad - doesn't improve readability list add element // prefer list.add(element)
2. Make Operations Feel Natural
The best infix functions read like English:
// Natural file writeTo directory user belongsTo group config overriddenBy userPrefs // Unnatural config.serialize() // this shouldn't be infix
3. Consider Chaining
Infix functions can be chained, but use this carefully:
// OK for mathematical operations val result = vector add otherVector scale 2.0 // Can become hard to read val user = User() hasName "John" hasAge 25 belongsTo adminGroup
Common Pitfalls to Avoid
1. Operator Precedence Confusion
// This might not do what you expect val result = 2 + 3 multiply 4 // Actually: 2 + (3 multiply 4) // Be explicit with parentheses when mixing operators val result = (2 + 3) multiply 4
2. Not Considering Java Interop
If your Kotlin code needs to be called from Java, remember that Java users will need to use the regular method syntax:
// Kotlin
val pair = "key" to "value"
// Java (this won't work in Java)
// Pair<String, String> pair = "key" to "value"; // Compile error
// Java (correct way)
Pair<String, String> pair = TuplesKt.to("key", "value");
Job Offers
Wrapping Up
Infix functions are one of those Kotlin features that seem simple on the surface but open up powerful possibilities for creating readable, expressive code. They can result in code that looks much more like a natural language, making your APIs more intuitive and your code more maintainable.
The range operators we use every day (.., downTo, until, step) are perfect examples of how infix functions can make everyday programming tasks feel more natural. And when you need to create domain-specific languages or fluent APIs, infix functions become an indispensable tool.
Remember the three rules: member or extension function, exactly one parameter, and the infix keyword. Follow these, use them judiciously, and you’ll find yourself writing code that not only works well but reads beautifully too.
Next time you’re designing an API or writing a utility function, ask yourself: “Would this read better as an infix function?” You might be surprised how often the answer is yes.
For any questions or queries, let’s connect on LinkedIn
This article was previously published on proandroiddev.com.


