Higher-Order Functions
function types ยท (T) -> RA higher-order function is one that takes another function as a parameter, returns one, or both. Kotlin makes this natural because function types are ordinary types โ (Int, Int) -> Int is a real type you can declare a variable with, pass around, and store in a data structure, the same as String or List<Int>. This is the foundation everything else on this page builds on: lambdas, extension functions, and most of the collections API all come down to functions accepting functions.
// The parameter `operation` has type (Int, Int) -> Int: // a function taking two Ints and returning an Int fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int { return operation(a, b) } val sum = calculate(3, 4) { x, y -> x + y } println(sum) // 7 // A named function reference works anywhere a lambda would fun multiply(x: Int, y: Int) = x * y println(calculate(3, 4, ::multiply)) // 12
// A function can return another function: the returned lambda // closes over `factor` from the enclosing scope fun multiplier(factor: Int): (Int) -> Int { return { n -> n * factor } } val triple = multiplier(3) println(triple(5)) // 15 println(triple(10)) // 30 val double = multiplier(2) println(double(5)) // 10: independent closure, own captured `factor`
func(int, int) int is a real type, and returning closures is idiomatic Go too. The syntax difference is mostly cosmetic: Kotlin's (Int, Int) -> Int reads left-to-right as "these params, this return," where Go's func(a, b int) int keeps the C-style declaration order.Lambda Syntax & it
{ x -> ... } ยท it ยท implicit single paramA lambda in Kotlin is always written inside curly braces, never with a fun keyword or parentheses around the parameter list โ that's how the compiler tells a lambda literal apart from a block. When a lambda takes exactly one parameter and you don't need to name it, Kotlin lets you drop the parameter declaration entirely and refer to it as it. This isn't magic scoping; it's a single, well-known implicit name the compiler makes available.
// Full form: parameter list, arrow, body โ all inside { } val add: (Int, Int) -> Int = { a, b -> a + b } println(add(2, 3)) // 5 // The last expression in the body is the return value: no "return" needed val describe: (Int) -> String = { n -> val parity = if (n % 2 == 0) "even" else "odd" "$n is $parity" // this is the lambda's result } println(describe(4)) // "4 is even"
val numbers = listOf(1, 2, 3, 4, 5) // Named parameter form numbers.filter { n -> n % 2 == 0 } // it: implicit name, valid only for single-parameter lambdas numbers.filter { it % 2 == 0 } // [2, 4] numbers.map { it * it } // [1, 4, 9, 16, 25] // Nested single-param lambdas shadow `it` โ name explicitly to avoid confusion numbers.map { outer -> numbers.filter { it > outer }.count() }
it for short, single-purpose lambdas where the meaning is obvious from context โ filter { it > 0 } reads fine. Once a lambda spans more than a line or two, or you're nesting lambdas, name the parameter explicitly. it inside it is a real readability trap, and linters like detekt flag it for exactly that reason.Trailing Lambda Syntax
last param ยท DSL shapeWhen a function's last parameter is a function type, Kotlin lets you move the lambda argument outside the parentheses โ and if the lambda is the only argument, you can drop the parentheses entirely. This single syntax rule is what makes Kotlin's standard library functions (let, run, apply, collection operators) and DSL builders (Gradle's Kotlin DSL, Compose) read like built-in language constructs instead of ordinary function calls.
fun repeat3(times: Int, action: (Int) -> Unit) { for (i in 0..<times) action(i) } // Fully explicit call: lambda passed as an ordinary argument repeat3(3, { i -> println("iteration $i") }) // Idiomatic: the trailing lambda moves outside () repeat3(3) { i -> println("iteration $i") } // When the lambda is the ONLY argument, () can be dropped entirely fun runTwice(action: () -> Unit) { action(); action() } runTwice { println("hi") } // no () before the { at all
// Trailing lambdas compose: a function taking a receiver lambda // (see extension functions below) plus trailing syntax is the whole // trick behind Gradle's build.gradle.kts and Kotlin HTML builders fun html(build: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.build() return sb.toString() } val page = html { append("<h1>") append("Hello") append("</h1>") } println(page) // <h1>Hello</h1>
doWork(3, func(i int) { ... }). Trailing lambda syntax is a purely Kotlin ergonomic win; there's nothing to unlearn here, just a shorter way to write the same call shape.Function References
:: ยท method refs ยท constructor refsThe :: operator turns an existing named function, method, property, or constructor into a value of function type, without wrapping it in a lambda by hand. It's most useful when a lambda would just forward its arguments to an existing function โ { x -> foo(x) } and ::foo do the same thing, and the reference is shorter and states the intent more directly.
fun isEven(n: Int) = n % 2 == 0 val numbers = listOf(1, 2, 3, 4, 5, 6) // Top-level function reference: no lambda wrapper needed println(numbers.filter(::isEven)) // [2, 4, 6] // Member reference: Type::method, bound at each call to its receiver val words = listOf("kotlin", "go", "rust") println(words.map(String::uppercase)) // [KOTLIN, GO, RUST] // Bound reference: fixed to one specific instance val greeting = "hello" val lengthOfGreeting = greeting::length println(lengthOfGreeting()) // 5 // Constructor reference: ::ClassName builds instances data class Point(val x: Int, val y: Int) val coords = listOf(1 to 2, 3 to 4) val points = coords.map { (x, y) -> Point(x, y) } // or, pointfree: coords.map(::Point) doesn't fit here since Point // needs destructuring first, but a single-arg constructor works directly: data class Id(val value: Int) val ids = listOf(1, 2, 3).map(::Id)
list.map { it.uppercase() } and list.map(String::uppercase) compile to essentially the same thing โ the reference form just says "apply this existing function" without the extra visual noise of a parameter name you never use for anything else.inline, noinline, crossinline
inline fun ยท noinline ยท crossinlineEvery lambda you write is, under the hood, an object implementing a function interface โ passing one to a higher-order function normally means allocating that object and paying a virtual call to invoke it. Marking a function inline tells the compiler to paste the function's body, and the lambda's body, directly into every call site at compile time. No object, no virtual call โ and as a bonus, return inside the lambda can return from the enclosing function, not just the lambda (a "non-local return").
// Without inline: block is an object, action() is a virtual call fun measure(block: () -> Unit): Long { val start = System.nanoTime() block() return System.nanoTime() - start } // inline: the compiler pastes measure()'s body AND the lambda's body // directly at the call site โ no allocation, no virtual dispatch inline fun measureInline(block: () -> Unit): Long { val start = System.nanoTime() block() return System.nanoTime() - start } // A non-local return: only legal because measureInline is inline โ // the lambda's body is pasted into processItems' own body fun processItems(items: List<Int>) { measureInline { for (item in items) { if (item < 0) return // returns from processItems, not just the lambda println(item) } } }
// noinline: opt one specific lambda parameter OUT of inlining // (needed when you must store the lambda or pass it onward as an object) inline fun process( inline action: () -> Unit, noinline callback: () -> Unit ) { action() storeForLater(callback) // callback must be a real object to store it } // crossinline: still inlined, but forbids non-local return โ // required when the lambda runs inside another execution context // (like a different thread or a nested function) where "return" from // the outer function wouldn't make sense inline fun runInBackground(crossinline action: () -> Unit) { Thread { action() }.start() // without crossinline, `return` inside action would try to return // from runInBackground after it may have already returned }
let, map, filter, forEach) as inline, which is exactly why non-local return works inside a forEach lambda but not inside a regular function-type parameter you declare yourself. See Gotchas for the flip side: what happens when you rely on non-local return without realizing the function isn't inline.inline by default. It increases compiled bytecode size at every call site (code duplication, effectively), so it only pays off for small, frequently-called functions โ the exact profile of scope functions and collection operators. See Performance for when inlining actually helps versus when it just bloats the binary.Extension Functions
fun T.name() ยท receiver ยท static dispatchAn extension function lets you add a member-call-syntax function to a type you don't own โ including types from the standard library or a third-party dependency โ without subclassing it or modifying its source. Inside the function body, this refers to the receiver, the instance the extension was called on. It's syntactic sugar over an ordinary static function taking the receiver as its first parameter; nothing about the original type actually changes; there is no reflection or monkey-patching involved.
// Extends String โ a type you can't modify โ with a new member fun String.isPalindrome(): Boolean { val cleaned = this.lowercase().filter { it.isLetterOrDigit() } return cleaned == cleaned.reversed() } println("racecar".isPalindrome()) // true println("A man, a plan, a canal: Panama".isPalindrome()) // true println("kotlin".isPalindrome()) // false
open class Animal class Dog : Animal() // Extension functions are chosen based on the DECLARED (compile-time) // type, not the runtime type โ unlike overridden member functions fun Animal.describe() = "an animal" fun Dog.describe() = "a dog" val animal: Animal = Dog() // declared type: Animal, runtime type: Dog println(animal.describe()) // "an animal" โ resolved by the declared type Animal val dog: Dog = Dog() println(dog.describe()) // "a dog"
IsPalindrome(s string) bool. Extension functions give you the same capability but with method call syntax at the use site: s.isPalindrome() reads better than IsPalindrome(s) when you're chaining several calls together.infix Functions
infix ยท to ยท one param onlyMarking a single-parameter member or extension function infix lets you call it without the dot or parentheses: a to b instead of a.to(b). It's a narrow feature with a narrow purpose โ making a small set of calls read like natural-language operators โ and the compiler enforces the constraint that keeps it from getting out of hand: exactly one parameter, no defaults, no vararg.
// to, from the standard library, builds a Pair โ the most common // infix function you'll use without even thinking about it val entry = 1 to "one" // same as 1.to("one"), produces Pair(1, "one") // Defining your own infix fun Int.until(other: Int): IntRange = this..<other println((1 until 5).toList()) // [1, 2, 3, 4] class Vector2(val x: Double, val y: Double) { infix fun dot(other: Vector2) = x * other.x + y * other.y } val v1 = Vector2(1.0, 2.0) val v2 = Vector2(3.0, 4.0) println(v1 dot v2) // 11.0
infix for functions that genuinely read like an operator or a natural sentence โ to, and, zip, step. Slapping it on ordinary methods just to save a dot and parens usually hurts readability rather than helping it; the standard library uses it sparingly for exactly this reason.Operator Overloading
operator fun ยท plus ยท times ยท get/setKotlin lets you define what +, *, [], and a fixed set of other operators mean for your own types, by implementing a function with a specific name and the operator modifier. It's not open-ended operator invention โ you can't create a new symbol โ but the closed list covers arithmetic, comparison, indexing, and invocation, which is enough to make math-like types (vectors, matrices, money) read naturally.
data class Vector2(val x: Double, val y: Double) { // a + b desugars to a.plus(b) operator fun plus(other: Vector2) = Vector2(x + other.x, y + other.y) // a * scalar desugars to a.times(scalar) operator fun times(scalar: Double) = Vector2(x * scalar, y * scalar) // unary minus: -a desugars to a.unaryMinus() operator fun unaryMinus() = Vector2(-x, -y) } val a = Vector2(1.0, 2.0) val b = Vector2(3.0, 4.0) println(a + b) // Vector2(x=4.0, y=6.0) println(a * 2.0) // Vector2(x=2.0, y=4.0) println(-a) // Vector2(x=-1.0, y=-2.0)
class Grid(val width: Int, val height: Int) { private val cells = IntArray(width * height) // grid[x, y] desugars to grid.get(x, y) operator fun get(x: Int, y: Int) = cells[y * width + x] // grid[x, y] = v desugars to grid.set(x, y, v) operator fun set(x: Int, y: Int, value: Int) { cells[y * width + x] = value } } val grid = Grid(10, 10) grid[2, 3] = 42 println(grid[2, 3]) // 42 // invoke: makes an instance callable like a function, obj(args) class Greeter(val greeting: String) { operator fun invoke(name: String) = "$greeting, $name!" } val greeter = Greeter("Hi") println(greeter("Ada")) // "Hi, Ada!" โ greeter("Ada") calls invoke
+ to mean something surprising (like triggering a network call) is the kind of thing that makes an interviewer's eyebrow go up.Local Functions
fun inside fun ยท closures over localsKotlin allows a full fun declaration nested inside another function body, not just a lambda. A local function can access and mutate the variables of its enclosing function directly โ it closes over them the same way a lambda would โ but it reads and debugs like an ordinary named function, which is often clearer than an equivalently-scoped lambda assigned to a local val.
fun validateOrder(items: List<Double>, discount: Double): Double { // Local function: only visible inside validateOrder, // and it can see `discount` from the enclosing scope directly fun applyDiscount(price: Double): Double { require(price >= 0) { "price can't be negative" } return price * (1 - discount) } return items.sumOf { applyDiscount(it) } } println(validateOrder(listOf(10.0, 20.0, 30.0), discount = 0.1)) // 54.0
fun helperName(...) more readable than val helperName = { ... } with an inferred type. Both close over the same enclosing state; it's purely a readability choice.Quick Reference
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Function type | (Int, Int) -> Int | A real, first-class type |
| Lambda literal | { a, b -> a + b } | Always in braces; last expr is the return value |
| Implicit param | { it * 2 } | Only for single-parameter lambdas |
| Trailing lambda | f(x) { ... } | Last function-type param moves outside () |
| Lambda-only call | f { ... } | () dropped entirely when the lambda is the only arg |
| Function reference | ::functionName | Points to a top-level function |
| Member reference | Type::method | Unbound; takes a receiver as its first param |
| Bound reference | instance::method | Fixed to one specific receiver instance |
| Constructor reference | ::ClassName | Builds instances, usable in map/etc. |
| Inline function | inline fun f(block: () -> Unit) | Body pasted at call site; enables non-local return |
| Opt out of inlining | noinline param | Needed to store or pass the lambda onward |
| Inline without non-local return | crossinline param | Needed when the lambda runs in another context |
| Extension function | fun T.name() { this... } | Resolved by declared (static) type, not runtime type |
| Infix call | a to b | Exactly one param, no default, no vararg |
| Operator overload | operator fun plus(o: T) | Enables a + b via a.plus(b) |
| Indexed access | operator fun get/set | Enables obj[i] and obj[i] = v |
| Callable instance | operator fun invoke(...) | Enables obj(args) |
| Local function | fun helper() { ... } inside fun | Closes over enclosing locals like a lambda would |