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.