Polyglot
Languages Kotlin functions
Kotlin § functions

Functions and lambdas

Kotlin functions are first-class values. The fun keyword introduces named functions; lambdas admit anonymous functions; function references (::name) admit treating functions as values. Functions admit default arguments, named arguments, vararg (variadic), single-expression form (fun f() = expr), infix calls, operator overloading, inline (admits substantial higher-order-function efficiency), tailrec (admits substantial recursion without stack growth), and extension functions (treated separately). The conventional Kotlin discipline favours trailing lambdas — when the last argument is a lambda, it admits being placed outside the parentheses. The combination — substantial parameter forms, single-expression functions, inline for efficiency, trailing lambdas, lambdas with receiver for DSLs — is the substance of Kotlin’s function surface.

Function declarations

The principal form:

fun name(parameters): ReturnType {
    body
}

Examples:

fun add(a: Int, b: Int): Int {
    return a + b
}

fun greet(name: String): String {
    return "Hello, $name"
}

fun performAction() {                              // Unit return type implicit
    println("acting")
}

The return type follows the colon; Unit (the empty type) is the default if omitted.

Single-expression functions

fun add(a: Int, b: Int): Int = a + b               // = expression
fun greet(name: String) = "Hello, $name"           // return type inferred
fun square(n: Int) = n * n
fun isAdult(age: Int) = age >= 18

The conventional discipline favours the = form for short single-expression functions.

Unit and Nothing returns

fun performAction(): Unit { /* ... */ }            // explicit Unit (rare)
fun performAction() { /* ... */ }                  // Unit implicit

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

Treated in Types.

Parameters

Default arguments

fun greet(name: String = "world", greeting: String = "Hello"): String {
    return "$greeting, $name"
}

greet()                                            // "Hello, world"
greet("Alice")                                     // "Hello, Alice"
greet("Alice", "Hi")
greet(greeting = "Hi")                             // named: uses default for name

Named arguments

The name = value syntax at the call site:

fun configure(host: String = "localhost", port: Int = 8080, timeout: Int = 30) {
    /* ... */
}

configure(host = "example.com")
configure(port = 9000, host = "example.com")       // any order
configure(timeout = 60)                            // skip middle args

The conventional discipline uses named arguments for substantial parameter lists — admit clarity without remembering positions.

vararg

The vararg admits any number of arguments of the same type:

fun sum(vararg nums: Int): Int = nums.sum()

sum()                                              // 0
sum(1, 2, 3)                                       // 6
sum(*intArrayOf(1, 2, 3))                          // 6 (spread)

Inside the function, nums is IntArray (for primitive types) or Array<T> (for object types).

The spread operator * admits unpacking an array into varargs:

val nums = intArrayOf(1, 2, 3)
sum(*nums)                                         // spread

inline parameters

The inline modifier on functions taking lambda parameters admits the compiler inlining the function body at the call site:

inline fun <T> measure(block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    println("took ${System.currentTimeMillis() - start}ms")
    return result
}

val data = measure { fetchData() }

The mechanism admits substantial efficiency for higher-order functions — eliminates the lambda allocation and admits non-local returns from the block.

noinline and crossinline

For inline functions with multiple lambda parameters:

inline fun example(
    inlineBlock: () -> Unit,
    noinline notInlined: () -> Unit,                // not inlined
    crossinline mustNotReturn: () -> Unit             // inlined; cannot return non-locally
) {
    inlineBlock()
    notInlined()
    mustNotReturn()
}

The principal uses are advanced: rare in routine application code.

tailrec

The tailrec admits tail-call optimisation — recursive functions that end with a recursive call admit substantial optimisation (no stack growth):

tailrec fun factorial(n: Long, acc: Long = 1): Long {
    return if (n <= 1) acc
            else factorial(n - 1, acc * n)         // tail call
}

factorial(10000)                                   // no stack overflow

The conventional uses are recursive algorithms for substantial inputs.

Function references

The ::name admits referring to a function as a value:

fun double(n: Int): Int = n * 2

val f: (Int) -> Int = ::double                     // function reference
val result = f(5)                                  // 10

val list = listOf(1, 2, 3).map(::double)           // [2, 4, 6]

// Method references:
val isEven: (Int) -> Boolean = Int::isMultipleOf2  // bound to instance
val toString: (Int) -> String = Int::toString      // unbound

// Constructor references:
val createUser: (String, Int) -> User = ::User
val users = listOf("Alice" to 30, "Bob" to 25).map { (name, age) ->
    User(name, age)
}

Lambdas

Anonymous functions:

val add = { a: Int, b: Int -> a + b }
val result = add(3, 4)                             // 7

val greet: (String) -> Unit = { name -> println("Hello, $name") }
greet("Alice")

The form: { params -> body }. The last expression is the return value.

Single-parameter lambdas

For lambdas with a single parameter, it is the implicit name:

listOf(1, 2, 3).map { it * 2 }                     // [2, 4, 6]
listOf(1, 2, 3).filter { it > 1 }                  // [2, 3]
listOf("a", "b").forEach { println(it) }

Trailing lambdas

When the last argument to a function is a lambda, it admits outside the parentheses:

listOf(1, 2, 3).map({ x -> x * 2 })                // lambda inside parens
listOf(1, 2, 3).map { x -> x * 2 }                 // trailing lambda
listOf(1, 2, 3).map { it * 2 }                     // implicit it

// With other args:
runCatching { fetchData() }
listOf(1, 2, 3).fold(0) { acc, x -> acc + x }      // fold(initial, lambda)

// Multiple trailing lambdas (since 1.4):
buildString {
    append("Hello, ")
    append("World!")
}

UIView.animate(0.5,
    animations = { view.alpha = 0 },
) {
    println("done")
}

The trailing-lambda form is conventional in DSLs and substantial higher-order patterns.

Lambdas with receiver

The lambda with receiver admits referring to a “this” object inside the lambda:

val builder = StringBuilder().apply {              // 'this' is the StringBuilder inside
    append("Hello")
    append(", ")
    append("World")
}

val result = with(builder) {                       // 'this' is the builder
    append("!")
    toString()
}

// Definition:
inline fun <T, R> T.run(block: T.() -> R): R = block()

// Custom DSL:
fun html(block: HtmlBuilder.() -> Unit): HtmlBuilder {
    val b = HtmlBuilder()
    b.block()                                      // call as if it were a method
    return b
}

val page = html {
    body {
        h1 { +"Hello" }
        p { +"Welcome" }
    }
}

The mechanism admits substantial DSL syntax — conventional in Gradle build scripts, Compose UI, kotlinx.html, etc.

Higher-order functions

Functions that take or return other functions:

fun applyTwice(f: (Int) -> Int, x: Int): Int = f(f(x))

val result = applyTwice({ it + 1 }, 5)             // 7
val result = applyTwice(::doubleIt, 5)             // function reference

fun makeAdder(n: Int): (Int) -> Int = { x -> x + n }

val add5 = makeAdder(5)
add5(3)                                            // 8

fun <T, R> apply(value: T, transform: (T) -> R): R = transform(value)

Function types

val handler: (String) -> Unit = { println(it) }
val callback: () -> Unit = { /* ... */ }
val transform: (Int, Int) -> Int = { a, b -> a + b }

// Receiver type:
val builder: StringBuilder.() -> Unit = { append("hello") }

// Nullable function:
val optional: (() -> Int)? = null

// Function returning function:
val adder: (Int) -> (Int) -> Int = { n -> { x -> x + n } }

The function type syntax is (In) -> Out. With receiver: Receiver.(In) -> Out.

Closures

Lambdas capture variables from the enclosing scope:

fun makeCounter(): () -> Int {
    var count = 0
    return {
        count++
        count
    }
}

val counter = makeCounter()
counter()                                          // 1
counter()                                          // 2
counter()                                          // 3

Each call to makeCounter produces a new closure with its own count.

infix functions

Functions marked infix admit a name b syntax:

infix fun Int.add(other: Int): Int = this + other

5 add 3                                            // 8
5.add(3)                                           // also admitted

// In stdlib:
val pair = "key" to "value"                        // Pair via infix to
val range = 1 until 10                             // until is infix

The conventional uses are concise binary operators (to, until, step).

The principal restrictions:

  • Member function or extension function.
  • Single parameter.
  • Not vararg and no default value.

Operator overloading

The operator modifier admits operator overloading:

data class Vec2(val x: Double, val y: Double) {
    operator fun plus(other: Vec2) = Vec2(x + other.x, y + other.y)
    operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar)
    operator fun unaryMinus() = Vec2(-x, -y)
    operator fun get(index: Int): Double = when (index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException()
    }
}

val a = Vec2(1.0, 2.0)
val b = Vec2(3.0, 4.0)
val c = a + b                                      // operator overload
val d = a * 2.0
val e = -a
val first = a[0]

Treated in Operators.

Common patterns

Default + named parameters

fun fetch(
    url: String,
    method: String = "GET",
    headers: Map<String, String> = emptyMap(),
    timeout: Int = 30
): Response {
    /* ... */
}

fetch(url = "https://example.com")
fetch(url = "https://example.com", method = "POST")
fetch(url = "https://example.com", headers = mapOf("Auth" to "token"), timeout = 60)

Higher-order function with measure

inline fun <T> measure(label: String, block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    println("[$label] took ${System.currentTimeMillis() - start}ms")
    return result
}

val data = measure("fetch") {
    api.fetchData()
}

Builder with apply

fun buildHttpRequest() = HttpRequest.Builder().apply {
    url("https://example.com")
    method("POST")
    header("Content-Type", "application/json")
    body(payload.toJson())
}.build()

DSL with lambda-with-receiver

class HtmlBuilder {
    private val parts = mutableListOf<String>()

    fun h1(content: String) {
        parts.add("<h1>$content</h1>")
    }

    fun p(content: String) {
        parts.add("<p>$content</p>")
    }

    override fun toString() = parts.joinToString("\n")
}

fun html(block: HtmlBuilder.() -> Unit): HtmlBuilder {
    return HtmlBuilder().apply(block)
}

val page = html {
    h1("Welcome")
    p("Hello, world")
}

Function reference with map

val numbers = listOf("1", "2", "3")
val parsed = numbers.map(String::toInt)            // [1, 2, 3]

val users = data.map(::User)                       // constructor reference

val getNames: (User) -> String = User::name
val names = users.map(getNames)

Tail-recursive function

tailrec fun fibonacci(n: Int, a: Long = 0, b: Long = 1): Long {
    return if (n == 0) a
            else fibonacci(n - 1, b, a + b)
}

fibonacci(10000)                                   // no stack overflow

Inline higher-order

inline fun <T> retry(attempts: Int = 3, block: () -> T): T {
    var lastError: Throwable? = null
    repeat(attempts) {
        try {
            return block()
        } catch (e: Throwable) {
            lastError = e
        }
    }
    throw lastError!!
}

val result = retry { fetchData() }

Lambda factory

fun multiplier(factor: Int): (Int) -> Int = { it * factor }

val double = multiplier(2)
val triple = multiplier(3)

double(5)                                          // 10
triple(5)                                          // 15

Function as parameter

fun <T> filter(items: List<T>, predicate: (T) -> Boolean): List<T> {
    val result = mutableListOf<T>()
    for (item in items) {
        if (predicate(item)) result.add(item)
    }
    return result
}

filter(numbers) { it > 0 }
filter(users) { it.isActive }

let for null-safe transformations

val email: String? = "alice@example.com"

val emailHash = email?.let { md5(it) }
val emailUpper = email?.uppercase()?.let { sanitize(it) }

// Chained:
val processed = input
    ?.trim()
    ?.takeIf { it.isNotEmpty() }
    ?.let { transform(it) }

Curry-like pattern

fun adder(a: Int) = { b: Int -> a + b }

val add5 = adder(5)
val add10 = adder(10)

add5(3)                                            // 8
add10(3)                                           // 13

also for chaining side effects

val user = User("Alice")
    .also { println("created: $it") }
    .also { saveToDatabase(it) }
    .also { sendWelcomeEmail(it) }

Higher-order function composition

infix fun <A, B, C> ((A) -> B).andThen(g: (B) -> C): (A) -> C {
    return { a -> g(this(a)) }
}

val pipeline = ::trim andThen ::uppercase andThen ::sanitize
val result = pipeline(input)

with for block-form

val length = with(StringBuilder()) {
    append("Hello, ")
    append("World!")
    length                                         // returns the value
}

Variadic forwarding

inline fun <T> log(vararg args: Any?, block: () -> T): T {
    val argString = args.joinToString(", ") { it.toString() }
    println("> calling with $argString")
    val result = block()
    println("< returned $result")
    return result
}

val n = log("foo", 42) { compute() }

Returning a function

fun selector(field: String): (User) -> Any? {
    return when (field) {
        "name" -> User::name
        "age" -> User::age
        "email" -> User::email
        else -> throw IllegalArgumentException("unknown field: $field")
    }
}

val getName = selector("name")
users.map(getName)

A note on the conventional discipline

The contemporary Kotlin function advice:

  • Use single-expression form (= expr) for short functions.
  • Use named arguments for substantial parameter lists.
  • Use defaults freely.
  • Use trailing lambdas — admit substantial conciseness.
  • Use it for single-parameter lambdas.
  • Use function references (::name) for substantial higher-order patterns.
  • Use inline for higher-order functions with substantial lambda overhead.
  • Use tailrec for substantial tail-recursive algorithms.
  • Use infix sparingly — for substantially natural binary operations.
  • Use lambdas with receiver for DSLs.
  • Use apply/also/let/run/with for fluent patterns.

The combination — multiple parameter forms (default, named, vararg, inline), single-expression functions, function references, lambdas with implicit it, trailing lambdas, lambdas with receiver, the substantial scoped functions (apply, also, let, run, with) — is the substance of Kotlin’s function surface. The discipline produces concise, expressive code with substantial flexibility for higher-order patterns and DSL design.