In Kotlin, Functions Don't Need a Class


In Java, there is no such thing as a function — only methods, each one trapped inside a class. Want a helper that doesn’t belong to any object? You make a final class full of static methods and call it StringUtils. Kotlin drops that ceremony. A function can live on its own, at the top level of a file, and you call it by name. The keyword is fun.

This is the second post in the series. We’ve met the basic types; now we’ll write the functions that move them around.

Declaring a function

The shape is fun, a name, parameters, and an optional return type after a colon:

fun add(a: Int, b: Int): Int {
    return a + b
}

Parameters always have their type written out — Kotlin infers local variables, but never parameters. The return type comes after the parameter list, mirroring how val count: Int puts the type after the name.

A function that returns nothing has the return type Unit, Kotlin’s stand-in for void. You almost never write it — it’s the default when you leave the return type off:

fun greet(name: String) {        // returns Unit
    println("Hello, $name")
}

Single-expression functions

When a function is just one expression, the braces and return are noise. Replace them with an =:

fun add(a: Int, b: Int): Int = a + b

And once the body is an expression, the compiler can infer its type, so you can drop the return type too:

fun add(a: Int, b: Int) = a + b        // returns Int
fun square(x: Int) = x * x             // returns Int
fun fullName(p: Person) = "${p.first} ${p.last}"   // returns String

This is the idiomatic form for the small, pure functions that make up most of a codebase. Keep the explicit return type on public API, where it’s documentation; drop it on short local helpers, where it’s clutter.

Default arguments

Java’s answer to optional parameters is the overload pile: five versions of the same method, each forwarding to the next. Kotlin gives a parameter a default value and the pile disappears:

fun connect(host: String, port: Int = 443, timeoutMs: Int = 5_000) {
    // ...
}

connect("example.com")              // uses both defaults
connect("example.com", 8080)        // overrides the port

One declaration covers every combination Java would have needed a separate overload for. (You saw the same mechanism on constructors — it’s the same feature; a constructor is just a function.)

Named arguments

There’s a catch with the call above: what does connect("example.com", 8080) pass — a port or a timeout? At the call site you can’t tell. Named arguments fix that, and they’re what makes defaults pleasant to use:

connect("example.com", timeoutMs = 10_000)   // skip port, set timeout
connect(host = "example.com", port = 8080)    // self-documenting

Naming an argument lets you skip over earlier defaults and supply only the one you care about — impossible with positional calls. It also kills the classic boolean-parameter mystery: setEnabled(true) tells you nothing, but setEnabled(enabled = true) reads itself.

vararg: any number of arguments

A vararg parameter accepts zero or more values, which arrive inside the function as an array:

fun sum(vararg numbers: Int): Int {
    var total = 0
    for (n in numbers) total += n
    return total
}

sum()              // 0
sum(1, 2, 3)       // 6

If you already have an array and want to pass its elements as separate arguments, the spread operator * unpacks it:

val nums = intArrayOf(1, 2, 3)
sum(*nums)         // 6

Functions don’t need a home

Because functions are top-level, where you put one is a design choice, not a rule the language forces on you.

A top-level function sits directly in a file, outside any class. This is where utility functions belong — no Utils class required:

// in file Math.kt
fun gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

A local function is declared inside another function, and it can see that function’s variables. Reach for one when a piece of logic is repeated within a single function but has no meaning outside it:

fun printReport(rows: List<String>) {
    fun separator() = println("-".repeat(40))

    separator()
    rows.forEach { println(it) }
    separator()
}

separator exists only inside printReport. It’s a way to remove duplication without leaking a helper into the wider namespace.

Final thoughts

The throughline: Kotlin treats a function as a thing in its own right, not a fragment that has to be smuggled inside a class. That single decision is what retires the static-utility class, the overload pile, and the telescoping constructor all at once — defaults and named arguments do the work that three separate Java patterns used to.

Practice: reinforce this with the Functions workbook — fifteen short problems with solutions you can reveal as you go.

We’ve been writing function bodies that branch and loop without explaining the syntax. That’s next: control flow and ranges, where if returns a value, for loops over a range, and there’s no ternary operator because there doesn’t need to be.