Skip to content

04 · Functions

Functions are top-level citizens in Kotlin — they don't need to live inside a class the way they must in Java. This module covers declaring functions, default and named arguments, and the compact single-expression syntax you'll see everywhere in idiomatic Kotlin code.

Basic function syntax

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

fun main() {
    println(greet("Alice"))
}
Hello, Alice!

Single-expression functions

When a function body is a single expression, you can drop the braces, return, and often the return-type annotation too (it's inferred).

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

// Single-expression form -- equivalent, much shorter
fun addShort(a: Int, b: Int): Int = a + b

// Return type inferred from the expression
fun square(n: Int) = n * n

fun main() {
    println(add(2, 3))
    println(addShort(2, 3))
    println(square(5))
}
5
5
25

Default arguments

Parameters can have default values, so callers only need to supply what differs from the default — no method-overloading juggling required.

fun buildGreeting(name: String, greeting: String = "Hello", punctuation: String = "!"): String {
    return "$greeting, $name$punctuation"
}

fun main() {
    println(buildGreeting("Alice"))                          // uses both defaults
    println(buildGreeting("Bob", "Hi"))                       // overrides greeting
    println(buildGreeting("Carol", punctuation = "?"))        // named arg, skips greeting
}
Hello, Alice!
Hi, Bob!
Hello, Carol?

Named arguments

Any argument can be passed by name, in any order — especially useful with several parameters of the same type, where position alone is error-prone.

fun createUser(name: String, age: Int, isAdmin: Boolean = false) {
    println("$name, age $age, admin=$isAdmin")
}

fun main() {
    createUser(name = "Dana", age = 28, isAdmin = true)
    createUser(age = 35, name = "Evan")   // order doesn't matter with named args
}
Dana, age 28, admin=true
Evan, age 35, admin=false

Varargs

vararg lets a function accept a variable number of arguments of the same type, collected into an array inside the function.

fun sum(vararg numbers: Int): Int {
    var total = 0
    for (n in numbers) total += n
    return total
}

fun main() {
    println(sum(1, 2, 3))
    println(sum(1, 2, 3, 4, 5))

    val values = intArrayOf(10, 20, 30)
    println(sum(*values))   // spread operator -- unpacks an array into vararg
}
6
15
60

Functions as first-class values

A function can be assigned to a variable or passed as an argument using a function reference (::name) or a lambda — the basis for Kotlin's higher-order function style, covered in depth in Level 2.

fun double(n: Int) = n * 2

fun main() {
    val operation: (Int) -> Int = ::double
    println(operation(5))   // 10

    val lambda: (Int) -> Int = { n -> n * 3 }
    println(lambda(5))      // 15
}
10
15

Local functions

Functions can be nested inside other functions, useful for small helpers that only make sense in one place.

fun processOrder(quantity: Int, price: Double): Double {
    fun applyDiscount(amount: Double): Double =
        if (quantity > 10) amount * 0.9 else amount

    val subtotal = quantity * price
    return applyDiscount(subtotal)
}

fun main() {
    println(processOrder(15, 2.0))   // 27.0 -- discount applied
    println(processOrder(5, 2.0))    // 10.0 -- no discount
}
27.0
10.0

How It Actually Works

Top-level functions like greet don't exist as a JVM concept — the bytecode verifier only understands methods that belong to a class. The compiler solves this the same way it does for top-level main: every top-level function in Functions.kt becomes a public static method on a synthetic class named FunctionsKt. That's why calling a top-level Kotlin function from Java looks like FunctionsKt.greet("Alice") — you're seeing the real generated class name.

Default arguments are not a JVM feature either (the JVM has no notion of "optional parameter"), so the compiler fakes them with two techniques working together: it generates one method that always takes every parameter, and at each call site that omits some, it synthesizes the missing values inline. For a public/open function with defaults, the compiler additionally emits an overload suffixed with a hidden bitmask parameter ($mask) — visible if you run javap on the compiled class — that tells the method body which parameters were actually supplied by the caller, so it knows which ones to replace with their default expressions. This is why giving a function default parameters that is meant to be called from Java requires @JvmOverloads: without it, Java only sees the single method with the full parameter list plus the hidden mask, none of the convenient overloads Kotlin callers get for free.

Named arguments are purely a compile-time convenience — the compiler resolves punctuation = "?" to the correct parameter position while generating the call, and the emitted bytecode is an ordinary method invocation with arguments in declared order. No parameter names survive into the bytecode's method signature (the JVM erases them, though -parameters debug metadata can optionally retain names for reflection).

Cheat sheet

Concept Syntax
Basic function fun name(param: Type): ReturnType { ... }
Single-expression function fun name(param: Type) = expression
Default argument fun f(x: Int = 10)
Named argument call f(x = 5, y = 10)
Varargs fun f(vararg nums: Int)
Spread operator f(*array)
Function reference ::functionName
Function type (Int) -> Int

🔀 See this in another language

Exercise

Write a single-expression function isPrime(n: Int): Boolean that checks whether n is a prime number. Then write a function describeNumbers with a vararg numbers: Int parameter and a default label: String = "Numbers" parameter that prints the label followed by each number and whether it's prime, using isPrime. Call it once with a few numbers and once more with named label argument.