In the previous tutorial, you learned about null safety. Now let’s learn about functions — the building blocks that organize your code into reusable pieces.
Kotlin functions are more powerful than Java methods. They support default parameters, named arguments, single-expression syntax, and much more.
In this tutorial, you will learn:
- Regular functions
- Single-expression functions
- Default parameters (no more method overloading)
- Named arguments
- Varargs
- Local functions
- Higher-order functions
- Extension functions
- Infix functions
Regular Functions
A function in Kotlin starts with the fun keyword:
fun add(a: Int, b: Int): Int {
return a + b
}
println(add(3, 5)) // 8
The format is: fun name(parameters): ReturnType { body }
Functions That Return Nothing
If a function does not return a value, its return type is Unit. You don’t need to write it:
// These two are the same
fun printGreeting(name: String): Unit {
println("Hello, $name!")
}
fun printGreeting2(name: String) {
println("Hello, $name!")
}
Unit is like void in Java. Kotlin adds it automatically when you don’t specify a return type.
Single-Expression Functions
When a function has only one expression, you can use = instead of curly braces and return:
// Regular function
fun multiply(a: Int, b: Int): Int {
return a * b
}
// Single-expression function — shorter and cleaner
fun multiply(a: Int, b: Int): Int = a * b
// Even shorter — Kotlin infers the return type
fun multiply(a: Int, b: Int) = a * b
More examples:
fun isEven(number: Int) = number % 2 == 0
fun formatName(first: String, last: String) = "$first $last"
fun subtract(a: Int, b: Int) = a - b
Use single-expression functions when the logic is simple. For complex functions, use the regular style with curly braces.
Default Parameters
In Java, if you want optional parameters, you need method overloading — multiple methods with different parameter lists. In Kotlin, you use default parameters:
fun greet(
name: String,
greeting: String = "Hello",
punctuation: String = "!"
): String {
return "$greeting, $name$punctuation"
}
println(greet("Alex")) // Hello, Alex!
println(greet("Sam", "Hi")) // Hi, Sam!
println(greet("Jordan", "Hey", "!!!")) // Hey, Jordan!!!
Parameters with default values can be skipped when calling the function. One function replaces three Java methods.
Real-World Example
fun createUser(
name: String,
age: Int,
email: String = "",
isActive: Boolean = true,
role: String = "user"
): String {
return "User($name, age=$age, email=$email, active=$isActive, role=$role)"
}
println(createUser("Alex", 25))
// User(Alex, age=25, email=, active=true, role=user)
println(createUser("Sam", 30, role = "admin", email = "sam@example.com"))
// User(Sam, age=30, email=sam@example.com, active=true, role=admin)
Named Arguments
Named arguments let you specify which parameter you are passing. This makes code much more readable:
fun sendEmail(
to: String,
subject: String,
body: String,
cc: String = "",
bcc: String = "",
isHtml: Boolean = false
): String {
val format = if (isHtml) "HTML" else "Plain Text"
return "Email to=$to, subject=$subject, format=$format"
}
// Without named arguments — hard to read
sendEmail("alex@example.com", "Hello", "Test body", "", "", true)
// With named arguments — clear and readable
sendEmail(
to = "alex@example.com",
subject = "Hello",
body = "Test body",
isHtml = true
)
Named arguments let you:
- Skip parameters with defaults (like
ccandbccabove) - Pass parameters in any order
- Make your code self-documenting
You can also mix positional and named arguments. Positional arguments must come first:
greet("Alex", punctuation = "?") // Hello, Alex?
Varargs
vararg lets a function accept a variable number of arguments:
fun sum(vararg numbers: Int): Int {
var total = 0
for (number in numbers) {
total += number
}
return total
}
println(sum(1, 2, 3)) // 6
println(sum(10, 20, 30, 40, 50)) // 150
println(sum()) // 0
Inside the function, the vararg parameter is an array. You can loop over it or use array functions.
The Spread Operator
To pass an existing array to a vararg function, use the spread operator *:
val numbers = intArrayOf(1, 2, 3, 4, 5)
println(sum(*numbers)) // 15
You can combine the spread operator with other arguments:
println(sum(100, *numbers, 200)) // 315
Vararg with Other Parameters
fun printAll(prefix: String, vararg messages: String) {
for (message in messages) {
println("$prefix $message")
}
}
printAll("[LOG]", "Starting app", "Loading data", "Done")
// [LOG] Starting app
// [LOG] Loading data
// [LOG] Done
A function can have at most one vararg parameter.
Local Functions
You can define functions inside other functions. Local functions can access variables from the outer function:
fun processOrder(items: List<String>, discount: Double): String {
// Local function — only accessible inside processOrder
fun formatItem(item: String): String {
return "- $item"
}
fun calculateTotal(count: Int): Double {
val basePrice = count * 10.0
return basePrice * (1 - discount)
}
val formatted = items.map { formatItem(it) }
val total = calculateTotal(items.size)
return "Order:\n${formatted.joinToString("\n")}\nTotal: $${"%.2f".format(total)}"
}
println(processOrder(listOf("Laptop", "Mouse", "Keyboard"), 0.1))
// Order:
// - Laptop
// - Mouse
// - Keyboard
// Total: $27.00
Local functions are useful for helper logic that is only needed in one place.
Higher-Order Functions
A higher-order function takes a function as a parameter or returns a function.
Taking a Function as a Parameter
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
The type (Int, Int) -> Int means “a function that takes two Ints and returns an Int.”
You can pass a function in three ways:
// 1. Function reference with ::
fun add(a: Int, b: Int): Int = a + b
println(calculate(10, 5, ::add)) // 15
// 2. Lambda expression
println(calculate(10, 5, { a, b -> a * b })) // 50
// 3. Trailing lambda — if the last parameter is a function, put it outside ()
println(calculate(10, 5) { a, b -> a - b }) // 5
Returning a Function
fun getGreeter(language: String): (String) -> String {
return when (language) {
"en" -> { name -> "Hello, $name!" }
"de" -> { name -> "Hallo, $name!" }
"es" -> { name -> "Hola, $name!" }
else -> { name -> "Hi, $name!" }
}
}
val englishGreeter = getGreeter("en")
val germanGreeter = getGreeter("de")
println(englishGreeter("Alex")) // Hello, Alex!
println(germanGreeter("Alex")) // Hallo, Alex!
Function References
Use :: to reference an existing function:
fun double(x: Int) = x * 2
fun triple(x: Int) = x * 3
fun applyToList(numbers: List<Int>, transform: (Int) -> Int): List<Int> {
return numbers.map(transform)
}
println(applyToList(listOf(1, 2, 3), ::double)) // [2, 4, 6]
println(applyToList(listOf(1, 2, 3), ::triple)) // [3, 6, 9]
Extension Functions
Extension functions let you add new functions to existing types without modifying them:
fun String.removeSpaces(): String {
return this.replace(" ", "")
}
fun Int.isPositive(): Boolean = this > 0
fun List<Int>.secondOrNull(): Int? {
return if (this.size >= 2) this[1] else null
}
Usage:
println("Hello World".removeSpaces()) // HelloWorld
println(5.isPositive()) // true
println((-3).isPositive()) // false
println(listOf(1, 2, 3).secondOrNull()) // 2
println(emptyList<Int>().secondOrNull()) // null
Inside the extension function, this refers to the object the function is called on. Extension functions feel like they belong to the type, but they don’t actually modify it.
Infix Functions
Infix functions can be called without the dot and parentheses. They must have exactly one parameter:
infix fun Int.power(exponent: Int): Long {
var result = 1L
repeat(exponent) { result *= this }
return result
}
infix fun String.concat(other: String): String = "$this $other"
// Regular call
println(2.power(10)) // 1024
// Infix call — reads like English
println(2 power 10) // 1024
println("Hello" concat "World") // Hello World
Kotlin’s standard library has some infix functions too:
val pair = "key" to "value" // Creates a Pair
val range = 1 until 10 // Range from 1 to 9
Practical Example: Validation
Functions help you break complex logic into small, readable pieces:
fun validateUsername(username: String): String {
fun isLongEnough(s: String) = s.length >= 3
fun isShortEnough(s: String) = s.length <= 20
fun hasNoSpaces(s: String) = !s.contains(" ")
fun startsWithLetter(s: String) = s.first().isLetter()
return when {
!isLongEnough(username) -> "Too short (minimum 3 characters)"
!isShortEnough(username) -> "Too long (maximum 20 characters)"
!hasNoSpaces(username) -> "No spaces allowed"
!startsWithLetter(username) -> "Must start with a letter"
else -> "Valid"
}
}
println(validateUsername("alex")) // Valid
println(validateUsername("ab")) // Too short (minimum 3 characters)
println(validateUsername("hello world")) // No spaces allowed
println(validateUsername("123abc")) // Must start with a letter
Summary
| Feature | Description |
|---|---|
fun name() | Regular function |
fun name() = expr | Single-expression function |
param: Type = default | Default parameter |
name = value | Named argument |
vararg | Variable number of arguments |
*array | Spread operator for vararg |
(Type) -> Type | Function type |
::function | Function reference |
fun Type.name() | Extension function |
infix fun | Infix function (no dot/parens) |
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In the next tutorial, Kotlin Tutorial #6: Control Flow — if, when, for, while, you will learn:
ifas an expression (returns a value)when— Kotlin’s powerful pattern matching- Ranges and
forloops whileanddo-whileloopsbreak,continue, and labels
This is part 5 of the Kotlin Tutorial series. Check out Part 4: Null Safety if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.