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
varargand 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
itfor single-parameter lambdas. - Use function references (
::name) for substantial higher-order patterns. - Use
inlinefor higher-order functions with substantial lambda overhead. - Use
tailrecfor substantial tail-recursive algorithms. - Use
infixsparingly — for substantially natural binary operations. - Use lambdas with receiver for DSLs.
- Use
apply/also/let/run/withfor 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.