Intermediate

Functions in Kotlin

Learn how to define and use functions in Kotlin - parameters, default and named arguments, recursion, higher-order functions, lambdas, and extension functions with Docker-ready examples

Functions are the building blocks of any Kotlin program. They let you package logic into reusable, named units, give it clear inputs and outputs, and compose larger programs out of small, testable pieces. You have already met one function — main() — in the Hello World tutorial. Now we will look at how to write your own.

Kotlin is a multi-paradigm language, blending object-oriented and functional styles, and its function support reflects that. Functions can be declared at the top level (no class required), as members of a class (methods), or even as values you store in variables and pass around. They are first-class citizens: a function can take other functions as parameters and return functions as results.

This dual nature is one of Kotlin’s defining features. You can write straightforward procedural code when that’s clearest, then reach for higher-order functions, lambdas, and closures when a functional approach fits better. In this tutorial you will learn how to define functions, supply default and named arguments, write recursive functions, work with higher-order functions and lambdas, and extend existing types with extension functions.

Defining Functions

A Kotlin function is declared with the fun keyword, followed by a name, a parameter list, and an optional return type after a colon. When a function’s body is a single expression, you can drop the braces and the return keyword entirely.

Create a file named Functions.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Functions in Kotlin

// A standard function: parameters with types, an explicit return type
fun add(a: Int, b: Int): Int {
    return a + b
}

// A single-expression function — the body is one expression
fun multiply(a: Int, b: Int): Int = a * b

// Default arguments let callers omit parameters
fun greet(name: String, greeting: String = "Hello"): String {
    return "$greeting, $name!"
}

// A function that returns no useful value has the return type Unit (optional)
fun logMessage(message: String) {
    println("LOG: $message")
}

// vararg accepts any number of arguments
fun sumAll(vararg numbers: Int): Int {
    var total = 0
    for (n in numbers) {
        total += n
    }
    return total
}

fun main() {
    println(add(3, 4))            // 7
    println(multiply(6, 7))       // 42

    // Default argument used
    println(greet("Kotlin"))      // Hello, Kotlin!
    // Default argument overridden
    println(greet("Kotlin", "Hi"))
    // Named arguments — order no longer matters
    println(greet(greeting = "Welcome", name = "Ada"))

    logMessage("Functions are first-class")

    println(sumAll(1, 2, 3, 4, 5)) // 15
}

Key things to notice:

  • Parameter types are required — Kotlin is statically typed, so every parameter declares its type.
  • Return types can be inferred for single-expression functions (fun multiply(a: Int, b: Int) = a * b also works), but spelling them out improves readability.
  • Default arguments remove the need for the overload explosion common in Java.
  • Named arguments make calls self-documenting and let you skip earlier defaults.
  • Unit is Kotlin’s equivalent of void; it is the return type when a function produces no meaningful value and can be omitted.

Recursion and Scope

A function can call itself. Recursion is a natural way to express problems defined in terms of smaller subproblems, such as factorials. Variables declared inside a function are local to it — they exist only while the function runs and are not visible elsewhere.

Create a file named Recursion.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Recursion and variable scope

val appName = "Calculator"   // top-level (global) — visible everywhere in the file

// Classic recursive factorial: n! = n * (n-1)!
fun factorial(n: Int): Long {
    return if (n <= 1) 1 else n * factorial(n - 1)
}

// Recursive Fibonacci
fun fib(n: Int): Int {
    return if (n < 2) n else fib(n - 1) + fib(n - 2)
}

fun main() {
    val limit = 5            // local — only visible inside main()
    println("$appName starting")
    println("factorial($limit) = ${factorial(limit)}")
    println("fib($limit) = ${fib(limit)}")
}

Here appName is a top-level value visible to every function in the file, while limit is local to main(). Kotlin uses if as an expression (it returns a value), so the recursive functions return the result of the if directly. For deep recursion, Kotlin also offers the tailrec modifier to optimize tail-recursive calls into loops, avoiding stack overflow.

Higher-Order Functions, Lambdas, and Closures

Because functions are first-class values, a function can accept another function as a parameter or return one. These are called higher-order functions. A lambda is a function written inline as an expression, and a closure is a function that captures variables from its surrounding scope.

Create a file named HigherOrder.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Higher-order functions, lambdas, and closures

// Takes a function (Int, Int) -> Int as its third parameter
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

// Returns a function — the returned lambda "closes over" factor
fun makeMultiplier(factor: Int): (Int) -> Int {
    return { value -> value * factor }
}

fun main() {
    // Pass a lambda directly (trailing-lambda syntax)
    val sum = calculate(5, 3) { x, y -> x + y }
    println("Sum: $sum")

    val product = calculate(5, 3) { x, y -> x * y }
    println("Product: $product")

    // Store a function in a variable
    val square: (Int) -> Int = { n -> n * n }
    println("Square of 6: ${square(6)}")

    // A closure that remembers its captured factor
    val triple = makeMultiplier(3)
    println("Triple 7: ${triple(7)}")

    // Built-in higher-order functions on collections
    val numbers = listOf(1, 2, 3, 4, 5)
    val doubled = numbers.map { it * 2 }
    val evens = numbers.filter { it % 2 == 0 }
    println("Doubled: $doubled")
    println("Evens: $evens")
}

Notable Kotlin conventions:

  • Trailing lambda syntax — when a function’s last parameter is a function, the lambda can go outside the parentheses: calculate(5, 3) { x, y -> x + y }.
  • it — a single-parameter lambda can refer to its argument as it instead of naming it, as in numbers.map { it * 2 }.
  • Function types are written (Int) -> Int, meaning “takes an Int, returns an Int.”
  • Closures capture variables (factor) from the enclosing scope, even after the outer function has returned.

Methods and Extension Functions

In Kotlin’s object-oriented side, a function declared inside a class is called a method (or member function) and operates on instances of that class. Kotlin also supports extension functions, which let you add new methods to existing types — even ones you didn’t write — without inheritance.

Create a file named Methods.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Member functions (methods) and extension functions

class Rectangle(val width: Int, val height: Int) {
    // A method — belongs to the class and can use its properties
    fun area(): Int = width * height

    // Methods can call other methods of the same class
    fun describe(): String = "Rectangle ${width}x$height with area ${area()}"
}

// An extension function adds a method to the String type
fun String.shout(): String = this.uppercase() + "!"

// An extension function on Int
fun Int.isEven(): Boolean = this % 2 == 0

fun main() {
    val rect = Rectangle(4, 5)
    println(rect.area())        // call a method with dot notation
    println(rect.describe())

    // Extension functions are called exactly like built-in methods
    println("hello".shout())
    println("10 is even: ${10.isEven()}")
    println("7 is even: ${7.isEven()}")
}

Inside an extension function, this refers to the receiver — the value the function is called on. Extension functions are resolved statically (they don’t truly modify the class), but they read just like ordinary methods at the call site, which makes APIs feel natural and discoverable.

Running with Docker

Each example has its own main(), so compile and run them one at a time. The Zenika image bundles the Kotlin compiler (kotlinc) and a JVM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Pull the official image
docker pull zenika/kotlin:1.4

# Run the basic functions example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc Functions.kt -include-runtime -d functions.jar && java -jar functions.jar"

# Run the recursion example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc Recursion.kt -include-runtime -d recursion.jar && java -jar recursion.jar"

# Run the higher-order functions example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc HigherOrder.kt -include-runtime -d higherorder.jar && java -jar higherorder.jar"

# Run the methods and extensions example
docker run --rm -v $(pwd):/app -w /app zenika/kotlin:1.4 \
  sh -c "kotlinc Methods.kt -include-runtime -d methods.jar && java -jar methods.jar"

Expected Output

Running Functions.kt:

7
42
Hello, Kotlin!
Hi, Kotlin!
Welcome, Ada!
LOG: Functions are first-class
15

Running Recursion.kt:

Calculator starting
factorial(5) = 120
fib(5) = 5

Running HigherOrder.kt:

Sum: 8
Product: 15
Square of 6: 36
Triple 7: 21
Doubled: [2, 4, 6, 8, 10]
Evens: [2, 4]

Running Methods.kt:

20
Rectangle 4x5 with area 20
HELLO!
10 is even: true
7 is even: false

Key Concepts

  • fun declares a function, with typed parameters and an optional return type; single-expression functions use = and need no braces or return.
  • Default and named arguments eliminate most overloading and make call sites self-documenting.
  • Unit is Kotlin’s void-equivalent return type and can be omitted for functions that return nothing useful.
  • Functions are first-class values — they can be stored in variables, passed as arguments, and returned from other functions.
  • Lambdas are inline function literals; the last lambda argument can sit outside the parentheses, and a single parameter is available as it.
  • Closures capture variables from their surrounding scope and keep them alive after the enclosing function returns.
  • Methods are functions that belong to a class, while extension functions add new methods to existing types without modifying or subclassing them.
  • Recursion is fully supported, and the tailrec modifier optimizes tail-recursive functions to avoid stack overflow.

Running Today

All examples can be run using Docker:

docker pull zenika/kotlin:1.4
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining