skip to content

Special Function Forms

The declaration modifiers and naming conventions that change how a function is called: infix, operator, tailrec, and invoke. Each one is small, but together they explain how Kotlin DSLs read the way they do.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

19

What is an infix function in Kotlin, and what are the requirements a function must satisfy to be callable in infix form?

level: juniorimportance: must knowfreq 55%

answer

  1. Marked `infix`, member or extension, exactly one param
  2. No vararg, no default value on the param
  3. Left = receiver, right = argument
  4. Readability sugar only — same as a.name(b)
  5. `to`, `until`, `downTo`, `shl`, `and`

basics

~10 s

An infix function lets you call it between two values without a dot or parentheses, like 1 to 2. It must be marked infix, be a member or extension, and take exactly one parameter.

solid answer

~40 s

An `infix` function can be invoked in the form `a foo b` instead of `a.foo(b)`. To qualify it must: (1) be a member function or an extension function; (2) be marked with the `infix` modifier; (3) accept exactly one parameter; (4) that parameter must not be a `vararg` and must not have a default value. The left operand is the receiver, the right is the argument. Common standard-library examples are `to` (builds a `Pair`), `until`, `downTo`, `step`, `shl`, `shr`, `and`, `or`, and `xor`. Infix exists purely for readability — `map.put(k, v)` versus `mapOf(a to b)` reads more like prose. It changes only call syntax, not semantics; you can always still call it with the normal dot form.

code

kotlin · 8 lines
kotlin
infix fun Int.times2(other: Int): Int = this * other

val a = 6 times2 7   // 42
val b = 6.times2(7)  // 42 — dot form still valid

// stdlib infix examples
val p = "key" to "value"      // Pair("key", "value")
val r = (1 until 5).toList()   // [1, 2, 3, 4]

go deeper

for a junior

States the infix modifier, member-or-extension, exactly-one-param rule, and gives to as an example.

for a middle

Adds the no-vararg / no-default constraint and notes receiver-vs-argument and that the dot form still works.

for a senior

Frames infix as pure readability sugar compiling to a.name(b) and distinguishes it from operator overloading.

for a principal

Discusses when infix improves API ergonomics vs. when it harms clarity, and library design conventions around it.

## What an infix function is An **infix function** is a function you can call by placing its name *between* two operands, with no dot and no parentheses: ```kotlin infix fun Int.shl(bits: Int): Int = this shl bits // (conceptual) val pair = 1 to 2 // instead of 1.to(2) val shifted = 1 shl 4 // instead of 1.shl(4) -> 16 ``` The value on the **left** of the function name is the **receiver** (`this`), the value on the **right** is the single **argument**. ## The four rules A function is eligible for infix calls only if ALL of these hold: - It is a **member function** (declared inside a class/object) **or** an **extension function** (declared as `fun Receiver.name(...)`). Top-level functions with no receiver cannot be infix. - It is marked with the **`infix`** modifier keyword. - It has **exactly one parameter**. - That parameter is **not `vararg`** and has **no default value**. ```kotlin infix fun String.repeated(times: Int): String = this.repeat(times) val s = "ab" repeated 3 // "ababab" val s2 = "ab".repeated(3) // identical — dot form always works ``` ## Why it exists Infix is **syntactic sugar for readability only**. It does not change dispatch, performance, or semantics. The classic motivation is DSL-like, prose-like code: `mapOf("a" to 1, "b" to 2)` reads naturally because `to` is infix. Range builders `1 until 10`, `10 downTo 1`, `1..10 step 2` rely on it too. ## Things that are NOT infix - Operators like `+` use **operator overloading** (the `operator` modifier), a different mechanism. - You still need the receiver and argument to be unambiguous; mixing infix calls with other operators requires understanding precedence (covered separately). Under the hood the compiler simply rewrites `a name b` into `a.name(b)`.

  • Can a top-level function with no receiver be infix?
    No. Infix functions must be a member or an extension, so they always have a receiver as the left operand.
  • Does adding `infix` remove the ability to call with a dot?
    No. `a foo b` and `a.foo(b)` are both valid; `infix` only adds the dot-less form.

Like writing '3 plus 4' in English instead of 'plus(3, 4)' — same operation, more readable word order.

saying these in an interview costs you the question

  • Claiming any function can be infix
  • Saying infix functions can take multiple parameters
  • Confusing infix with operator overloading (`operator` modifier)
  • Thinking infix changes performance or dispatch
  • Allowing a default value or vararg on the infix parameter

context

open as a page

What does the `invoke` operator function do in Kotlin, and how do you make an instance of your own class callable like a function?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Declaring operator fun invoke(...) on a class lets you call an instance with parentheses, like obj(), instead of writing obj.invoke(). It turns an object into something you can call like a function.

open as a page

What is operator overloading in Kotlin, and how do you make the `+` symbol work on your own class?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Operator overloading lets symbols like + or [] work on your own types. You write a function with a fixed name (like plus) and mark it with the operator keyword. Then a + b calls a.plus(b).

open as a page

How does the standard-library `to` function work, and how do `until`, `downTo`, and `step` use infix to make range code read naturally?

level: middleimportance: must knowfreq 50%

basics

~20 s

to is an infix extension that builds a Pair, so "a" to 1 makes Pair("a", 1). until, downTo, and step are infix functions that build ranges/progressions, letting you write 0 until n or 10 downTo 1 step 2.

open as a page

How do indexing (`a[i]`) and call (`a()`) syntax map to functions via operator overloading? Show `get`, `set`, and `invoke`.

level: middleimportance: must knowfreq 60%

basics

~10 s

a[i] calls a.get(i), and a[i] = v calls a.set(i, v). a() calls a.invoke(). Each must be declared with the operator keyword.

open as a page

Why does `tailrec fun factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)` fail to optimize, and how do you fix it?

level: middleimportance: must knowfreq 50%

basics

~10 s

The recursive call isn't the last thing the function does — it multiplies by n afterwards. Move the running product into an extra parameter so the call becomes the final operation.

open as a page

What does the `tailrec` modifier do in Kotlin, and why would you use it?

level: juniorimportance: should knowfreq 55%

basics

~10 s

tailrec tells the compiler to turn a function that calls itself at the very end into a plain loop. This avoids the program crashing with a stack overflow on deep recursion.

open as a page

Where do infix function calls sit in Kotlin's operator precedence, and how does that affect an expression like `1 + 2 shl 3` or `a == b and c`?

level: middleimportance: should knowfreq 40%

basics

~10 s

Infix calls have lower precedence than arithmetic operators (+, -, *) but higher than comparisons, equality, and the logical &&/||. So arithmetic binds first, then infix, then comparisons.

open as a page

How can you define `invoke` on a companion object to create a factory that looks like a constructor call, and why would you do that?

level: middleimportance: should knowfreq 45%

basics

~10 s

Put operator fun invoke(...) inside a class's companion object. Then ClassName(...) actually calls that factory method, letting you add logic (validation, caching, choosing a subtype) while still looking like a normal constructor.

open as a page

Explain how Kotlin function types and lambdas relate to the `invoke` operator. What is `FunctionN` and how does calling a lambda work under the hood?

level: middleimportance: should knowfreq 40%

basics

~20 s

A function type like (Int) -> String is really an interface with one method, invoke. A lambda is an object implementing that interface. Calling f(x) just calls f.invoke(x), so lambdas and invoke are the same mechanism.

open as a page

How do comparison operators (`<`, `>=`), `in`, and `..` work through operator conventions? Explain `compareTo`, `contains`, and `rangeTo`.

level: middleimportance: should knowfreq 50%

basics

~10 s

All comparison operators (<, <=, >, >=) use one compareTo function returning an Int. a in b calls b.contains(a). a..b calls a.rangeTo(b) to make a range.

open as a page

When is it appropriate to add the `infix` modifier to an API, and what are the design pitfalls? Give an example of a well-designed infix function.

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use infix only when the call reads like a natural two-word phrase (x shouldBe y). Avoid it when the operand order is unclear, when there are side effects, or when precedence with other operators could confuse readers.

open as a page

Explain how `++`, `--`, and augmented assignments (`+=`) are resolved via operator conventions, including the `plus` vs `plusAssign` ambiguity.

level: seniorimportance: should knowfreq 40%

basics

~20 s

++ uses inc() and -- uses dec(), each returning a new value. a += b first tries plusAssign (mutating); if absent it falls back to a = a + b using plus. Defining both can cause an ambiguity error.

open as a page

What are the limits and constraints of `tailrec` in Kotlin? When can the compiler NOT apply the optimization?

level: seniorimportance: should knowfreq 38%

basics

~10 s

tailrec only works when a function calls itself directly as its very last step. It can't optimize mutual recursion, calls inside try/catch, open functions, or any call where work happens afterward.

open as a page

Inside a class, how does an infix member function bind to `this`, and what subtle issue arises when you call an infix member from within the same class without specifying a receiver?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

An infix member's left operand is its receiver. If you call it inside the class without a left operand, you must write this name arg explicitly — bare name arg doesn't work, because infix needs an explicit left side.

open as a page

Can `invoke` be overloaded and can it be a suspend function? Show how multiple `invoke` overloads resolve and how a suspending callable object behaves.

level: seniorimportance: nice to knowfreq 25%

basics

~10 s

Yes. A type can declare several invoke overloads with different parameters, and the compiler picks the matching one. invoke can also be marked suspend, so calling obj(...) works inside a coroutine and can suspend.

open as a page

By what mechanism does Kotlin resolve an operator to a function, and what are the constraints and pitfalls (return types, extensions, ambiguity, when NOT to overload)?

level: seniorimportance: nice to knowfreq 30%

basics

~10 s

Kotlin matches an operator to a fixed function name plus its parameter signature, requiring the operator keyword. Some operators constrain return types. Overloading by signature is allowed, but overusing operators can hurt readability.

open as a page

When is making a type callable via `invoke` good API design, and when is it harmful? What readability and tooling trade-offs should guide the decision?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Use invoke when the type is genuinely 'one action' (a strategy, use-case, or DSL builder) so obj(args) reads naturally. Avoid it when the type has many responsibilities — a named method is clearer and easier to search and document.

open as a page

What does the compiler actually generate for a `tailrec` function, and what are the engineering trade-offs of using it versus a hand-written loop?

level: principalimportance: nice to knowfreq 22%

basics

~10 s

The compiler turns the recursion into a plain while(true) loop that updates the parameters and jumps back. So at runtime it's the same as a hand-written loop — you just get cleaner, recursive-looking source.

open as a page