Functions in Kotlin
A function in Kotlin is a named, reusable block of code that takes input, performs an action, and optionally returns a result. Kotlin functions are more flexible than Java methods: they can be declared at the top level of a file with no enclosing class required, they support default parameter values and named arguments so you rarely need method overloading, and because functions are first-class values you can pass them around like any other piece of data. This lesson walks through everything from a basic function declaration to vararg parameters and functions that accept other functions as arguments.
Overview: How Functions Work in Kotlin
Every Kotlin function begins with the fun keyword, followed by a name, a parenthesized parameter list, an optional return type, and a body. Unlike Java, a function does not need to live inside a class — a file full of top-level functions is completely normal Kotlin. Under the hood, the compiler still has to produce JVM bytecode, so top-level functions in a file named Utils.kt end up as static methods on a generated class called UtilsKt. You never have to write that class yourself; the compiler does it for you.
Each parameter must have an explicit type — Kotlin infers the types of local variables, but never of function parameters, because that would make a function’s contract ambiguous to callers. Parameters are also non-null by default: a parameter declared as name: String can never receive null, and the compiler rejects any call site that tries. If a parameter should accept a missing value, its type must be explicitly nullable, written as String?.
The return type comes after the parameter list, separated by a colon. If you omit it, Kotlin assumes Unit, which is Kotlin’s equivalent of void — except Unit is a real type with a single instance, not a compiler-only placeholder. For a function whose entire body is one expression, you can skip the curly braces and return keyword entirely and use = instead; this is called a single-expression function, and the compiler infers the return type from the expression on the right. The one exception is recursive functions: because the compiler cannot infer a type for a function while it is still analyzing a call to itself, a recursive single-expression function must declare its return type explicitly.
Kotlin also lets you give a parameter a default value with = someValue in the declaration. Callers can then omit that argument entirely, which removes most of the need for Java-style method overloading. Combined with default values, Kotlin supports named arguments — you can pass arguments as parameterName = value in any order at the call site, which is especially useful when a function has several parameters of the same type or several booleans, where a plain positional call would be easy to misread.
A function can accept a variable number of arguments of the same type using the vararg modifier; inside the function body, that parameter behaves like an array. If you already have an array and want to pass its contents as individual vararg arguments, you must unpack it with the spread operator, *array.
Finally, functions are values in Kotlin. A function type is written as (ParamTypes) -> ReturnType, and you can store a function in a variable, pass it as an argument, or return it from another function. When a function’s last parameter has a function type, Kotlin lets you write the corresponding lambda argument outside the parentheses — trailing lambda syntax — which is why standard library calls like list.map { it * 2 } read so cleanly. You can also declare a local function nested inside another function; a local function can see and use the parameters and local variables of its enclosing function, similar to a closure.
Syntax
fun functionName(param1: Type1, param2: Type2 = defaultValue): ReturnType {
// function body
return result
}
// Single-expression form
fun functionName(param1: Type1): ReturnType = expression
| Part | Meaning |
|---|---|
fun |
Keyword that starts every function declaration. |
functionName |
The identifier used to call the function. |
param1: Type1 |
A parameter name and its required, explicit type. |
= defaultValue |
Optional default value; callers may omit this argument at the call site. |
: ReturnType |
The type of value returned; omit it (or write Unit explicitly) if the function returns nothing meaningful. |
{ ... } / = expression |
A block body with an explicit return, or a single-expression body whose value is the return value. |
Examples
Example 1: A Basic Function
fun greet(name: String): String {
return "Hello, $name!"
}
fun main() {
val message = greet("Kotlin")
println(message)
}
Output:
Hello, Kotlin!
The function greet takes one non-null String parameter and returns a String built with a string template. Inside main, the result is stored in a val (since it never needs to change) and printed. Note that name can never be null here; if you tried to call greet(null), the compiler would reject the program before it ever ran.
Example 2: Default and Named Parameters
fun greetUser(name: String, greeting: String = "Hello"): String = "$greeting, $name!"
fun main() {
println(greetUser("Ava"))
println(greetUser("Liam", "Hi"))
println(greetUser(greeting = "Welcome", name = "Noah"))
}
Output:
Hello, Ava!
Hi, Liam!
Welcome, Noah!
greetUser is a single-expression function: its body is one expression after =, and Kotlin infers the return type as String automatically. The greeting parameter has a default value, so the first call only supplies name and falls back to "Hello". The second call overrides the default positionally. The third call uses named arguments and supplies them in reverse order — perfectly legal, because Kotlin matches named arguments by name, not position.
Example 3: Vararg Parameters and Functions as Arguments
fun sumAll(vararg numbers: Int): Int = numbers.sum()
fun applyOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int = operation(a, b)
fun main() {
println(sumAll(1, 2, 3, 4))
val product = applyOperation(6, 7) { x, y -> x * y }
println(product)
}
Output:
10
42
sumAll accepts any number of Int arguments, which arrive inside the function as an IntArray, so calling .sum() on it works directly. applyOperation takes two integers and a third parameter of function type (Int, Int) -> Int. Because that function-typed parameter is last, the call applyOperation(6, 7) { x, y -> x * y } can write the lambda outside the parentheses — trailing lambda syntax — which is the idiomatic way Kotlin passes behavior into a function.
How It Works Step by Step
When Kotlin evaluates a function call, it first evaluates every argument expression left to right (substituting default values for any omitted parameters), binds each resulting value to its parameter, then executes the function body. For a block body, execution stops at the first return reached; for a single-expression body, the value of the expression is the return value with no explicit return needed. Local functions add one more wrinkle: because they are declared inside another function’s body, they are only visible inside that enclosing function, and they can read variables from the enclosing scope.
fun processOrder(itemPrice: Int, quantity: Int): Int {
fun applyTax(amount: Int): Int = amount + amount / 10
val subtotal = itemPrice * quantity
return applyTax(subtotal)
}
fun main() {
val total = processOrder(20, 3)
println(total)
}
Output:
66
Step by step: main calls processOrder(20, 3), so itemPrice is bound to 20 and quantity to 3. The local function applyTax is defined but not run yet. Next, subtotal is computed as 20 * 3 = 60. Finally applyTax(60) runs, adding ten percent (60 / 10 = 6) to get 66, which processOrder returns and main prints. applyTax only exists for the duration of this call and is invisible to any other function in the file.
Common Mistakes
Mistake 1: Recursive single-expression function without an explicit return type
fun factorial(n: Int) = if (n <= 1) 1 else n * factorial(n - 1)
This fails to compile. When a single-expression function calls itself, the compiler needs a declared return type before it can finish analyzing the recursive call — it cannot infer a type it hasn’t finished computing yet. The fix is to add the return type explicitly:
fun factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)
fun main() {
println(factorial(5))
}
Output:
120
Mistake 2: Passing an array to a vararg parameter without the spread operator
fun sumAll(vararg numbers: Int): Int = numbers.sum()
fun main() {
val values = intArrayOf(1, 2, 3)
println(sumAll(values))
}
This does not compile: sumAll expects individual Int arguments, but values is a single IntArray, and Kotlin does not implicitly unpack it. Use the spread operator, *values, to tell the compiler to expand the array into individual vararg arguments:
fun sumAll(vararg numbers: Int): Int = numbers.sum()
fun main() {
val values = intArrayOf(1, 2, 3)
println(sumAll(*values))
}
Output:
6
Mistake 3: Relying on parameter position when a default value isn’t last
fun registerUser(name: String, active: Boolean = true, email: String): String =
"$name ($email) active=$active"
fun main() {
println(registerUser("Sam", "sam@example.com"))
}
This fails to compile. The second positional argument, "sam@example.com", is matched against active: Boolean because that is the second parameter in the declaration — a String cannot be assigned to a Boolean, so it’s a type mismatch. Whenever a default parameter isn’t last, callers who want to skip it must use a named argument for whatever comes after it:
fun registerUser(name: String, active: Boolean = true, email: String): String =
"$name ($email) active=$active"
fun main() {
println(registerUser("Sam", email = "sam@example.com"))
}
Output:
Sam (sam@example.com) active=true
Best Practices
- Use single-expression functions (
=) for short, one-line logic, but give recursive or public API functions an explicit return type even when Kotlin could otherwise infer it — it documents the contract and avoids the recursion error above. - Reach for default parameter values instead of writing several overloaded functions the way you might in Java.
- Use named arguments whenever a call has multiple parameters of the same type, or any boolean parameter, so the call site reads clearly without needing to check the function signature.
- Where possible, put parameters with default values after the required parameters so callers can still call the function positionally without naming every argument.
- Keep a function-type parameter last in the parameter list so callers can use trailing lambda syntax.
- Extract local functions for helper logic that only makes sense inside one enclosing function, instead of polluting the file with a top-level helper nobody else should call.
- Prefer
valfor values inside a function body, and give every function a single, clearly named responsibility rather than one long function that does several unrelated things.
Practice Exercises
- Write a function
isEven(n: Int): Booleanthat returns whethernis divisible by two, then call it inmainfor the numbers4,7, and10, printing each result. - Write a single-expression function
describe(name: String, age: Int = 18): Stringthat returns a sentence like"Noah is 18 years old."using a string template. Call it once without anageargument and once with an explicit age, and print both results. - Write a higher-order function
repeatAction(times: Int, action: () -> Unit)that callsactionthe given number of times. Call it with trailing lambda syntax to print"Kotlin!"three times. Expected output isKotlin!printed on three separate lines.
Summary
- Every function starts with
fun, has explicitly typed parameters, an optional return type (defaulting toUnit), and either a block body withreturnor a single-expression body after=. - Recursive single-expression functions must declare their return type explicitly, since the compiler cannot infer it mid-recursion.
- Default parameter values plus named arguments remove most of the need for Java-style overloading and keep call sites readable.
- A
varargparameter behaves like an array inside the function; use the spread operator*arrayto pass an existing array’s contents as vararg arguments. - Functions are values: a parameter of function type like
(Int, Int) -> Intlets you pass behavior into a function, and a trailing lambda as the last argument keeps the call site clean. - Local functions declared inside another function are scoped to it and can read its parameters and local variables.
