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¶
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))
}
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
}
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
}
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
}
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
}
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
}
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.