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 cc and bcc above)
  • 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

FeatureDescription
fun name()Regular function
fun name() = exprSingle-expression function
param: Type = defaultDefault parameter
name = valueNamed argument
varargVariable number of arguments
*arraySpread operator for vararg
(Type) -> TypeFunction type
::functionFunction reference
fun Type.name()Extension function
infix funInfix function (no dot/parens)

Source Code

You can find the complete source code for this tutorial on GitHub:

KT-5 Source Code on GitHub

What’s Next?

In the next tutorial, Kotlin Tutorial #6: Control Flow — if, when, for, while, you will learn:

  • if as an expression (returns a value)
  • when — Kotlin’s powerful pattern matching
  • Ranges and for loops
  • while and do-while loops
  • break, 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.