Functions
Declaring functions, default arguments, named parameters, lambdas, and higher-order functions. Functions are first-class citizens in Kotlin.
By the end of this chapter, you will:
- Declare functions with parameters and return types
- Use default arguments and named parameters
- Write single-expression functions
- Create and pass lambda expressions
- Understand higher-order functions and the
itkeyword
Declaring Functions
Functions in Kotlin start with the fun keyword. The parameter type comes after the parameter name, separated by a colon.
// Function with parameters and return type
fun greet(name: String): String {
return "Hello, $name!"
}
// Function with no return value (returns Unit, like void)
fun printGreeting(name: String) {
println("Hello, $name!")
}
// Calling functions
val message = greet("Alex")
println(message) // Hello, Alex!
val — you cannot reassign them. This trips up JavaScript and Python developers who are used to mutating parameters.
fun doubleIt(n: Int): Int {
// n = n * 2 // ❌ ERROR: Val cannot be reassigned
return n * 2 // ✅ Create a new value instead
}
// Function with receiver type — extends existing classes
fun String.addExclamation(): String = this + "!"
val excited = "Hello".addExclamation() // "Hello!"
// Local function — scoped inside another function
fun factorial(n: Int): Int {
fun helper(x: Int, acc: Int): Int =
if (x <= 1) acc else helper(x - 1, acc * x)
return helper(n, 1)
}
// Function returning a function — creates a multiplier
fun makeMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
val triple = makeMultiplier(3)
println(triple(4)) // 12
// Closure modifying captured variable
var sum = 0
listOf(1, 2, 3).forEach { sum += it }
println(sum) // 6
Compare with JS / Python
function doubleIt(n) {
n = n * 2; // Allowed (but doesn't affect caller)
return n;
}
def double_it(n):
n = n * 2 # Allowed (rebinds local name)
return n
Kotlin difference: Kotlin prevents parameter reassignment at compile time. This avoids bugs where you accidentally mutate a parameter thinking it affects the caller. It's also a form of defensive programming — parameters are inputs, not local variables.
Compare with JS / Python
function greet(name) {
return `Hello, ${name}!`;
}
const greet = (name) => `Hello, ${name}!`;
def greet(name: str) -> str:
return f"Hello, {name}!"
Kotlin difference: Parameter types are required and come after the name (name: String). Return type comes after parameters (): String). No implicit return.
Default Arguments & Named Parameters
This is one of Kotlin's best features — inherited from Python but with static type safety.
fun createUser(
name: String,
age: Int = 0, // default value
isActive: Boolean = true // default value
) {
println("$name, $age, active=$isActive")
}
// All of these are valid:
createUser("Alex") // Alex, 0, active=true
createUser("Alex", 25) // Alex, 25, active=true
createUser("Alex", isActive = false) // Alex, 0, active=false
createUser(name = "Alex", age = 25) // named parameters — order doesn't matter!
sendEmail(to, from, true, false, null) becomes sendEmail(to, from, encrypt = true).
Compare with JS / Python
function createUser(name, age = 0, isActive = true) {
console.log(name, age, isActive);
}
createUser("Alex");
createUser("Alex", undefined, false); // ugly!
def create_user(name, age=0, is_active=True):
print(name, age, is_active)
create_user("Alex")
create_user("Alex", is_active=False) # named args
Kotlin difference: Very similar to Python! But Kotlin checks types at compile time, so you get safety + convenience.
Single-Expression Functions
When a function body is a single expression, you can omit the curly braces and use =.
// Instead of this:
fun add(a: Int, b: Int): Int {
return a + b
}
// Write this:
fun add(a: Int, b: Int): Int = a + b
// Return type can even be inferred:
fun multiply(a: Int, b: Int) = a * b
Lambdas & Higher-Order Functions
Functions are first-class in Kotlin. You can pass them as arguments, return them, and store them in variables.
Lambda Syntax
// A lambda (anonymous function)
val square: (Int) -> Int = { x -> x * x }
// If there's only one parameter, it's called `it` automatically
val double: (Int) -> Int = { it * 2 }
// Using lambdas
println(square(5)) // 25
println(double(5)) // 10
Higher-Order Functions
A higher-order function is a function that takes another function as a parameter or returns one.
// Takes a function as parameter
fun operateOn(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
// Usage
val sum = operateOn(3, 4) { x, y -> x + y } // 7
val product = operateOn(3, 4) { x, y -> x * y } // 12
// Trailing lambda syntax: if lambda is last arg, put it outside parentheses
val max = operateOn(3, 4) { x, y ->
if (x > y) x else y
}
More Lambda Patterns
Kotlin lambdas support destructuring, function references, and anonymous functions for cases where you need explicit return types.
// Destructuring in lambdas — iterate over maps cleanly
val headers = mapOf("Content-Type" to "application/json", "Auth" to "Bearer token")
headers.forEach { (key, value) ->
println("$key: $value")
}
// Anonymous function — useful when you need explicit return type
val numbers = listOf(1, 2, 3, 4, 5)
val odds = numbers.filter(fun(n: Int): Boolean {
return n % 2 != 0
})
// Function references — pass existing functions instead of lambdas
fun isOdd(n: Int) = n % 2 != 0
val oddsRef = numbers.filter(::isOdd) // reference to top-level function
val toIntRef = listOf("1", "2", "3").map(String::toInt) // reference to member function
•
{ } — short inline functions•
fun() { } — multiple return points or explicit return types•
::name — when you already have a function that does exactly what you need
Compare with JS / Python
const square = x => x * x;
function operateOn(a, b, operation) {
return operation(a, b);
}
const sum = operateOn(3, 4, (x, y) => x + y);
square = lambda x: x * x
def operate_on(a, b, operation):
return operation(a, b)
sum_val = operate_on(3, 4, lambda x, y: x + y)
Kotlin difference: The it convention and trailing lambda syntax make Kotlin lambdas especially clean. The type system ensures your lambda matches the expected signature at compile time.
Vararg Parameters
Accept a variable number of arguments with vararg (like *args in Python or ...args in JS).
fun sumAll(vararg numbers: Int): Int {
return numbers.sum()
}
sumAll(1, 2, 3) // 6
sumAll(1, 2, 3, 4, 5) // 15
// Spread operator to pass an array
val nums = intArrayOf(1, 2, 3)
sumAll(*nums) // 6
Local Functions
Kotlin lets you declare functions inside other functions. This keeps helper logic private and organized. Local functions can also capture variables from the outer scope — this is called a closure.
fun validateUser(user: User): Boolean {
// Local helper function — only visible inside validateUser
fun isEmailValid(email: String): Boolean {
return email.contains("@") && email.contains(".")
}
fun isAgeValid(age: Int): Boolean {
return age in 13..120
}
return isEmailValid(user.email) && isAgeValid(user.age)
}
// Closure: local function captures outer variable
fun processScores(scores: List): Map> {
val passingGrade = 60 // outer variable
// Local function captures `passingGrade` from the enclosing scope
fun categorize(score: Int): String {
return if (score >= passingGrade) "pass" else "fail"
}
return scores.groupBy { categorize(it) }
}
// processScores(listOf(85, 42, 73, 55))
// → {pass=[85, 73], fail=[42, 55]}
Generic Functions
Generic functions work with any type while maintaining type safety. They're essential for writing reusable utilities — in fact, most of Kotlin's standard library functions like filter, map, and listOf are generic.
// Generic function — works with any type T
fun asList(vararg ts: T): List {
val result = ArrayList()
for (t in ts) result.add(t)
return result
}
val stringList = asList("a", "b", "c") // List
val intList = asList(1, 2, 3) // List
// Type constraints — T must be Comparable
fun > sort(list: List): List {
return list.sorted()
}
// Multiple constraints using where clause
fun findMax(list: List): T
where T : Comparable {
return list.maxOrNull() ?: throw IllegalArgumentException("Empty list")
}
Int, String, or any Comparable. You've already been using generics: List<String>, map { it * 2 }, and filter { it > 0 } are all generic under the hood.
Tail Recursion
Deep recursion can cause a StackOverflowError. Kotlin's tailrec modifier optimizes recursive calls into loops, eliminating stack growth.
// Without tailrec: each call adds a stack frame
fun factorial(n: Int): Int {
return if (n <= 1) 1 else n * factorial(n - 1)
}
// With tailrec: compiler converts recursion to a loop
// The recursive call must be the LAST operation (tail position)
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 1) acc else factorial(n - 1, n * acc)
}
// Real-world: processing nested data structures safely
tailrec fun flattenPaths(current: String, remaining: List, acc: List = emptyList()): List {
return when {
remaining.isEmpty() -> acc
else -> {
val next = current + "/" + remaining.first()
flattenPaths(next, remaining.drop(1), acc + next)
}
}
}
// This won't crash even with thousands of nested items
val paths = flattenPaths("/home", listOf("user", "docs", "projects", "kotlin"))
n * factorial(n - 1)), the compiler can't optimize it and will show a warning.
tailrec for algorithms that naturally express as recursion but may process deep data — tree traversal, path resolution, state machines, and mathematical sequences. In functional programming styles, tail recursion replaces loops while keeping the code declarative.
Infix Functions
Infix functions let you call a function without dots or parentheses, making code read more like natural language or mathematical notation.
// Define an infix extension function
infix fun Int.times(str: String) = str.repeat(this)
// Call it like an operator
val result = 2 times "Bye " // "Bye Bye "
// Another example: range check
infix fun Int.isMultipleOf(n: Int) = this % n == 0
val check = 10 isMultipleOf 5 // true
// Real-world: building DSL-like APIs for tests or configuration
infix fun String.shouldBe(expected: String) {
if (this != expected) throw AssertionError("Expected '$expected' but was '$this'")
}
"Hello" shouldBe "Hello" // reads like English!
// Kotlin standard library uses infix extensively
val range = 1..10 // .. is an infix function
val map = mapOf("a" to 1) // to is an infix function creating a Pair
• DSLs and testing frameworks
• When a function represents a natural relationship between two values (like
to for pairs)• Kotlin's
to and until are built-in infix examplesDon't overuse — infix calls can hurt readability if the operation isn't intuitive.
📖 Official docs: Functions (kotlinlang.org) · Higher-Order Functions and Lambdas (kotlinlang.org)
Try It Yourself
Write a function formatPrice that takes a Double and an optional currency parameter (default "$"), and returns a formatted string like "$19.99" or "€19.99".
// Your code here:
// Should work:
// println(formatPrice(19.99)) // $19.99
// println(formatPrice(19.99, "€")) // €19.99
Stuck? Show hint
fun formatPrice(price: Double, currency: String = "$") = "$currency$price" — but this shows all decimals. Try String.format("%s%.2f", currency, price) for 2 decimal places.
Concept Check
What does this code print?
fun greet(name: String, greeting: String = "Hello") = "$greeting, $name!"
println(greet("Alex", greeting = "Hi"))