Free Handbook · Every example compiled & verified

Functions & Lambdas

Declare functions with default, named and vararg parameters, then treat functions as values: lambdas, function types, trailing lambdas, higher-order functions and tailrec.

0 / 136 lessons🔥 0 day streak
ShareXLinkedIn

Module 03 · what you'll be able to do

  • Write block-body and single-expression functions, and know when the return type can be left out
  • Call functions with default and named arguments, and pass any number of values with vararg and the spread operator
  • Explain Unit and Nothing, and use local functions to keep helpers private to one function
  • Write lambdas and function types, use it and trailing-lambda syntax, and build your own higher-order functions
  • Write a recursive function, trace it, and make a tail-recursive one stack-safe with tailrec
01

Declaring functions

A function starts with fun, then its name, its parameters in parentheses (each written name: Type) and its return type after a colon. Parameters are read-only — you cannot reassign them inside the function. When the whole body is one expression, drop the braces and return and write = expression: a single-expression function. The compiler can infer the return type of a single-expression function; a block body with a result always needs it written.

kotlinMain.kt
// Block body: braces, explicit return type, return statement
fun bmi(weightKg: Double, heightM: Double): Double {
    val value = weightKg / (heightM * heightM)
    return value
}

// Single-expression function: return type inferred as Boolean
fun isAdult(age: Int) = age >= 18

fun category(bmi: Double): String = when {
    bmi < 18.5 -> "under"
    bmi < 25.0 -> "normal"
    else -> "over"
}

fun main() {
    val b = bmi(70.0, 1.75)
    println("%.1f".format(b))
    println(category(b))
    println(isAdult(17))
}
Outputcompiled & run with real Kotlin
22.9
normal
false

Functions declared at the top of a file, outside any class, are top-level functions. Kotlin does not need a class to hold them, unlike Java.

Your turn

Write fun circleArea(r: Double) = … as a single-expression function using Math.PI, and print the area for radius 2.

Error you will hit

A block body with no return

kotlin
fun square(x: Int): Int {
    x * x
}

fun main() {
    println(square(4))
}
Main.kt:3:1: error: missing return statement.
Why the compiler said that

In a block body the last expression is not returned automatically — that rule is for lambdas and if/when branches. The function promised an Int and reaches its closing brace without handing one back.

The fix

Add return, or turn it into a single-expression function.

kotlin
fun square(x: Int): Int = x * x

fun main() {
    println(square(4))
}
02

Default, named and vararg parameters

A parameter can have a default value, so callers may leave it out. At the call site you can name arguments — greet(name = "Ravi", punctuation = "?") — which lets you skip defaults in the middle and makes calls with several booleans or numbers readable. Together they replace most of the overloads a Java class needs. A vararg parameter accepts any number of values and arrives in the function as an array; to pass an existing array into it, put the spread operator * in front.

kotlinMain.kt
fun greet(name: String, greeting: String = "Hello", punctuation: String = "!") =
    "$greeting, $name$punctuation"

fun average(vararg scores: Int): Double =
    if (scores.isEmpty()) 0.0 else scores.sum().toDouble() / scores.size

fun main() {
    println(greet("Asha"))
    println(greet("Asha", "Hi"))
    println(greet("Asha", punctuation = "?"))
    println(greet(punctuation = ".", name = "Ravi"))

    println(average())
    println(average(80, 90))
    val more = intArrayOf(70, 75, 95)
    println(average(60, *more))
}
Outputcompiled & run with real Kotlin
Hello, Asha!
Hi, Asha!
Hello, Asha?
Hello, Ravi.
0.0
85.0
75.0
Your turn

Add a parameter shout: Boolean = false to greet that uppercases the result, and call it with shout = true.

Error you will hit

No value passed for a parameter

kotlin
fun area(width: Int, height: Int): Int = width * height

fun main() {
    println(area(5))
}
Main.kt:4:13: error: no value passed for parameter 'height'.
Why the compiler said that

height has no default, so every call must supply it. Kotlin never fills a missing argument with zero or null.

The fix

Pass the argument, or give the parameter a default if a sensible one exists (here: a square).

kotlin
fun area(width: Int, height: Int = width): Int = width * height

fun main() {
    println(area(5))
}
Named arguments in code review
createUser("Asha", true, false, 3) tells a reviewer nothing. createUser("Asha", isAdmin = true, sendEmail = false, retries = 3) documents itself. Teams commonly require names for boolean and numeric literals in calls.
03

Unit, Nothing and local functions

A function that returns no useful value returns Unit — Kotlin's version of void, except that it is a real type with a single value, so it works with generics. You never write : Unit or return Unit. Nothing is the type of a function that never returns: it always throws or loops forever. Because Nothing fits anywhere a value is expected, val n = input.toIntOrNull() ?: fail("…") type-checks as an Int. The standard TODO() and error() functions return Nothing.

A local function is declared inside another function. It can read the outer function's parameters and variables, and nothing outside can call it — a clean way to name a repeated step without polluting the file.

kotlinMain.kt
fun log(message: String) {          // returns Unit
    println("[log] $message")
}

fun fail(reason: String): Nothing = throw IllegalArgumentException(reason)

fun parseAge(text: String): Int = text.toIntOrNull() ?: fail("not a number: $text")

fun report(name: String, scores: List<Int>) {
    fun line(label: String, value: Any) = println("$name $label: $value")  // local function

    line("count", scores.size)
    line("best", scores.max())
    line("mean", scores.average())
}

fun main() {
    val result = log("starting")
    println(result)                    // the single Unit value

    println(parseAge("42") + 1)
    try {
        parseAge("forty")
    } catch (e: IllegalArgumentException) {
        println("caught: ${e.message}")
    }

    report("Meera", listOf(70, 92, 81))
}
Outputcompiled & run with real Kotlin
[log] starting
kotlin.Unit
43
caught: not a number: forty
Meera count: 3
Meera best: 92
Meera mean: 81.0

Exceptions and try/catch get a full treatment in Module 11; here they only show that fail never returns normally.

Your turn

Give the local function line access to a new outer variable val unit = "pts" and print it after each value.

TypeMeaningJava equivalent
UnitReturns, but with no useful valuevoid
NothingNever returns (always throws)None
AnyAny non-null valueObject
Nothing?Can only be nullNone
04

Lambdas and function types

A lambda is a function without a name, written in braces: { a: Int, b: Int -> a + b }. Parameters go before the arrow, the body after it, and the value of the last expression is the result — no return. Every function value has a function type that reads like its signature: (Int, Int) -> Int takes two Ints and returns an Int; () -> Unit takes nothing and returns nothing. When a lambda has exactly one parameter, you may skip declaring it and call it it. To use an existing named function as a value, take a function reference with ::name.

kotlinMain.kt
fun isEven(n: Int) = n % 2 == 0

fun main() {
    val add: (Int, Int) -> Int = { a, b -> a + b }
    val shout = { s: String -> s.uppercase() + "!" }
    val double: (Int) -> Int = { it * 2 }      // single parameter: it
    val hello: () -> Unit = { println("hello from a lambda") }

    println(add(2, 3))
    println(shout("hey"))
    println(double(21))
    hello()

    val numbers = listOf(1, 2, 3, 4, 5, 6)
    println(numbers.filter(::isEven))          // function reference
    println(numbers.map(double))
    println(numbers.map { n -> n * n })
    println(numbers.count { it > 3 })
}
Outputcompiled & run with real Kotlin
5
HEY!
42
hello from a lambda
[2, 4, 6]
[2, 4, 6, 8, 10, 12]
[1, 4, 9, 16, 25, 36]
3
Your turn

Write val between: (Int, Int, Int) -> Boolean that checks whether the first number lies between the other two, and call it.

Trailing lambda syntax: if a function's last parameter is a function, you may put the lambda outside the parentheses — numbers.fold(0) { acc, n -> acc + n }. If the lambda is the only argument, the parentheses disappear entirely: numbers.map { it * 2 }. This is why so much Kotlin reads like built-in syntax: repeat(3) { }, thread { } and Compose's Column { } are all ordinary function calls.

Error you will hit

A bare return inside a lambda

kotlin
fun main() {
    val check = { n: Int ->
        if (n < 0) return
        n * 2
    }
    println(check(3))
}
Main.kt:3:20: error: 'return' is prohibited here.
Why the compiler said that

A plain return always means "return from the enclosing fun" — here, main. A lambda stored in a variable could be called long after main has finished, so jumping out of main from inside it is impossible.

The fix

Usually the cleanest fix is to make the body an if expression, as below. If you need an early exit, label the lambda and return from it: val check = check@{ n: Int -> if (n < 0) return@check 0; n * 2 }. A lambda passed to a function gets that function's name as a free label, as in return@forEach.

kotlin
fun main() {
    val check = { n: Int ->
        if (n < 0) 0 else n * 2
    }
    println(check(3))
}
05

Higher-order functions, closures and inline

A higher-order function takes a function as a parameter, returns one, or both. map, filter and forEach are higher-order functions from the standard library; writing your own is no harder. A lambda can read and even modify variables from the scope where it was created — it captures them, forming a closure. A function that returns a lambda can therefore hand out a little piece of private state.

kotlinMain.kt
// Takes a function
fun applyTwice(x: Int, f: (Int) -> Int): Int = f(f(x))

// Takes a function and returns a new one
fun logged(name: String, f: (Int) -> Int): (Int) -> Int = { x ->
    val result = f(x)
    println("$name($x) = $result")
    result
}

// Returns a closure that captures its own counter
fun makeCounter(): () -> Int {
    var count = 0
    return { ++count }
}

fun main() {
    println(applyTwice(3) { it * 10 })

    val square = logged("square") { it * it }
    square(4)
    square(9)

    val a = makeCounter()
    val b = makeCounter()
    a(); a()
    println("a=${a()} b=${b()}")
}
Outputcompiled & run with real Kotlin
300
square(4) = 16
square(9) = 81
a=3 b=1

Each call to makeCounter() creates a new count, so the two counters never share state.

Your turn

Write fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int that returns { g(f(it)) }, and test it.

inline, briefly. Each lambda passed to a normal function becomes an object at runtime. Marking the higher-order function inline makes the compiler paste the function body and the lambda into the call site instead, so no object is allocated. Most standard collection functions are inline. A side effect you will notice: inside a lambda passed to an inline function, a plain return returns from the enclosing function (a non-local return), while return@forEach only ends the current lambda call.

kotlinMain.kt
inline fun measure(label: String, block: () -> Unit) {
    println("start $label")
    block()
    println("end $label")
}

fun firstNegative(numbers: List<Int>): Int? {
    numbers.forEach {
        if (it < 0) return it        // non-local: returns from firstNegative
    }
    return null
}

fun main() {
    measure("work") { println("working...") }

    println(firstNegative(listOf(4, -2, 7, -9)))
    println(firstNegative(listOf(1, 2)))

    listOf(1, 2, 3, 4).forEach {
        if (it == 2) return@forEach   // skips just this element
        print("$it ")
    }
    println()
}
Outputcompiled & run with real Kotlin
start work
working...
end work
-2
null
1 3 4 
Your turn

Change return@forEach to plain return. What stops printing, and why?

06

Recursion and tailrec

A recursive function calls itself on a smaller version of the problem until it reaches a base case that it answers directly. Every call waits on the stack for the one below it, so very deep recursion throws StackOverflowError. When the recursive call is the very last thing the function does — nothing is left to compute after it returns — mark the function tailrec and the compiler rewrites it as a loop that uses no extra stack.

kotlinMain.kt
fun sumDigits(n: Int): Int =
    if (n < 10) n                      // base case
    else n % 10 + sumDigits(n / 10)    // smaller problem

// The recursive call is the last operation, so tailrec applies
tailrec fun gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a % b)

tailrec fun sumTo(n: Long, acc: Long = 0): Long =
    if (n == 0L) acc else sumTo(n - 1, acc + n)

fun main() {
    println(sumDigits(4096))
    println(gcd(48, 18))
    println(sumTo(1_000_000))   // a million calls deep - fine with tailrec
}
Outputcompiled & run with real Kotlin
19
6
500000500000

Remove tailrec from sumTo and the last line throws StackOverflowError on a default JVM stack.

Your turn

Write tailrec fun power(base: Long, exp: Int, acc: Long = 1): Long and compute 2 to the 20th.

VisualizesumDigits(4096)Step 1 / 7
fun sumDigits(n: Int): Int =
if (n < 10) n
else n % 10 + sumDigits(n / 10)
fun main() {
println(sumDigits(4096))
}
Line 6

main calls sumDigits(4096).

Variables now
n4096
All 7 steps as a table
StepLineWhat happenedVariables now
16main calls sumDigits(4096).n = 4096
234096 is not below 10, so compute 6 + sumDigits(409). The 6 waits on the stack.n = 4096 waiting = 6 + ?
33In the new call, 9 + sumDigits(40) waits.n = 409 waiting = 6 + 9 + ?
430 + sumDigits(4) waits.n = 40 waiting = 6 + 9 + 0 + ?
52Base case: 4 is below 10, so this call returns 4 without recursing.n = 4
63The calls unwind in reverse: 0 + 4 = 4, then 9 + 4 = 13, then 6 + 13 = 19.waiting = none
76The first call returns 19 to main.
Why sumDigits cannot be tailrec
After sumDigits(n / 10) returns, there is still an addition to do, so the call is not in tail position. Marking it tailrec only earns a compiler warning. The fix is the accumulator pattern used by sumTo: carry the running answer in a parameter. Module 13 uses recursion for trees and graphs.
07

Scope functions: a first look

Now that lambdas are familiar, the five scope functions are easy to read: each is a higher-order function that runs a lambda with an object in scope. let and also call the object it; run, with and apply make it this, so you can call its members directly. let, run and with return the lambda's result; apply and also return the object itself. Module 09 compares them in depth.

kotlinMain.kt
fun main() {
    // let: transform a value, result = lambda result
    val doubled = "kotlin".let { it.length * 2 }
    println(doubled)

    // apply: configure an object, result = the object
    val sb = StringBuilder().apply {
        append("a")
        append("b")
    }
    println(sb)

    // also: a side effect in the middle of a chain
    val total = listOf(3, 4, 5)
        .also { println("summing $it") }
        .sum()
    println(total)

    // run and with: compute something using this
    println("text".run { uppercase() + length })
    println(with(listOf(1, 2, 3)) { "size=$size first=${first()}" })
}
Outputcompiled & run with real Kotlin
12
ab
summing [3, 4, 5]
12
TEXT4
size=3 first=1
Your turn

Use apply to fill a mutableListOf<String>() with three names, then also to print its size.

Local variables beat this
Inside run, with and apply, a bare name is looked up in local scope before the object's members. When this example was first written, the variable on its first line was called length — and "text".run { uppercase() + length } silently printed TEXT12, picking up the local 12 instead of the string's length. Write this.length when there is any doubt.
Single-expression function
fun f(x: Int) = x * 2 — the body is one expression and the return type can be inferred.
Default argument
A parameter value used when the caller leaves the argument out.
Named argument
An argument passed as name = value, in any order.
vararg
A parameter that accepts any number of values; inside the function it is an array. Spread an existing array with *.
Unit
The return type of a function with no useful result; Kotlin's void, with a single value.
Nothing
The type of an expression that never completes normally, such as throw.
Lambda
An anonymous function in braces: { a, b -> a + b }. Its last expression is its result.
Function type
The type of a function value, such as (Int) -> Boolean or () -> Unit.
Higher-order function
A function that takes or returns another function.
Closure
A lambda together with the variables it captured from the scope where it was created.
Trailing lambda
Writing the last function argument outside the parentheses: list.fold(0) { acc, x -> acc + x }.
tailrec
Modifier that turns a tail-recursive function into a loop, so it cannot overflow the stack.
Quick check

Which call is valid for fun box(w: Int, h: Int = 1, d: Int = 1)?

Quick check

What does val f: (Int) -> Int = { it + 1 }; println(listOf(1, 2).map(f).map(f)) print?

Frequently asked questions

What is the difference between a function and a lambda in Kotlin?
A function declared with fun has a name and can use return, default arguments and vararg. A lambda is an anonymous function value in braces; its last expression is its result. Both can be stored and passed around: a lambda directly, a named function through a reference like ::isEven.
Does Kotlin support default parameters and named arguments?
Yes. Give a parameter a default with = value and callers can leave it out; name any argument at the call site with name = value. Together they replace most overloaded methods. Java callers do not see defaults unless you add @JvmOverloads.
When should I use tailrec?
When a recursive function could go deep (thousands of calls) and its recursive call is the last operation it performs. The compiler turns it into a loop. If the call is not in tail position, rewrite it with an accumulator parameter first; otherwise the modifier has no effect and you get a warning.

Finish the Kotlin handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.