1. Default values
When you’re just getting started, it can seem like the “swapped parameters” problem is something from the world of big projects. But in practice it shows up very early: as soon as a function appears with 3–5 parameters of the same type (for example, several Int or several Boolean), your brain starts tricking you. And it’s not that “you’re inattentive”—it’s normal human biology: we don’t have a built-in signature parser.
The solution in Kotlin is simple and effective: some parameters can be made optional via default values, and at the call site you can label arguments with their names. This turns a function call from a magic spell into a clear sentence you can read like normal text.
Making a parameter optional
Let’s start simple: very often a function is called in a typical way, and only sometimes you want to tweak a detail. For example, “print a header” almost always prints a line of = with the same width—but sometimes you want it wider or with a different character. In such cases, a default value is like a setting you usually don’t need to touch.
The syntax is very simple: in the parameter declaration, write = …. Kotlin uses that value if you don’t pass an argument for that parameter at the call site.
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printHeader("Wallet") // prints width 30 and '='
printHeader("Wallet", width = 10) // prints width 10 and '='
}
Notice an important point: we are not making “two versions” of the function. There is one function, but it has “default settings”. This is almost always better for a project than creating many similar functions with slightly different names.
A default is not only a constant
It may seem like a default value must be something simple like 0, true, or "text". But Kotlin allows any expression, including one that depends on other parameters. This is very convenient: for example, “the default length equals the array size”, or “the default limit equals the string length”.
There’s only one important rule: if the default depends on another parameter, that “other parameter” must come earlier in the parameter list (logically: you can’t refer to something that doesn’t exist “yet”).
A small example from our “Wallet” project: we want to print a list of expenses, and by default print the entire list.
fun printExpenses(amounts: IntArray, size: Int, limit: Int = size) {
var i = 0
while (i < size && i < limit) {
println("Expense #${i + 1}: ${amounts[i]}") // for example: Expense #1: 150
i++
}
}
fun main() {
val amounts = intArrayOf(150, 40, 999)
val size = 3
printExpenses(amounts, size) // prints 3 expenses
printExpenses(amounts, size, limit = 1) // prints 1 expense
}
Parameter order: required first, defaulted last
Now an important but logical idea. Default values make a parameter optional—which means you can omit it. Therefore, such parameters are almost always more convenient closer to the end of the list, and required ones closer to the beginning, so the compiler can reliably understand which argument is which.
Kotlin has a special rule: if a parameter with a default comes before a parameter without a default, then at the call site you can omit that parameter only by using a named argument.
Let’s make an example of “how not to do it”:
fun greeting(userId: Int = 0, message: String) {
println("[$userId] $message")
}
fun main() {
greeting(message = "Hi!") // [0] Hi!
greeting("Hi!") // not allowed: the compiler will complain
}
This order is sometimes needed (it happens), but more often it’s just poor design. In any tasks—and in our app—it’s better to follow a simple rule: “the most important goes first, settings go later”.
For example, here’s a more pleasant variant:
fun greeting(message: String, userId: Int = 0) {
println("[$userId] $message")
}
fun main() {
greeting("Hi!") // [0] Hi!
greeting("Hi!", 42) // [42] Hi!
}
2. Named arguments
Labeling arguments and stopping being afraid of identical types
Named arguments are needed not because Kotlin loves verbosity, but because they drastically reduce the chance of a mistake. Especially when parameters have the same type, or when the value itself doesn’t explain the meaning (classic case: arguments true, false, true—“what was that?”).
Syntax: at the call site write parameterName = value. Kotlin allows listing named arguments in any order—exactly for readability.
Let’s make a function for adding an expense to arrays (no categories yet, so we don’t get ahead of ourselves). Let’s say we have a “date” as day and month—primitive, but perfect for an example.
fun addExpense(
amounts: IntArray,
days: IntArray,
months: IntArray,
size: Int,
amount: Int,
day: Int,
month: Int
): Int {
amounts[size] = amount
days[size] = day
months[size] = month
return size + 1
}
fun main() {
val amounts = IntArray(10)
val days = IntArray(10)
val months = IntArray(10)
var size = 0
size = addExpense(amounts, days, months, size, amount = 150, day = 14, month = 1)
println(size) // 1
}
If we called it positionally as addExpense(…), it would look like a “phone number”. With names, the meaning is obvious immediately: amount, day, month.
Changing a setting without passing extra stuff
A common scenario: a function has several parameters with defaults, and you want to change not the first of them, but some other one. Kotlin allows this, and it’s useful to remember a simple rule: if you skip default parameters, then whatever you set after that is usually easier (and safer) to set as named.
The documentation phrases it like this: you can skip some arguments with defaults, but after the first skipped argument you need to name all subsequent ones (otherwise it’s unclear what maps to what).
Example: a report-printing function. By default we print both the list and the total, and the currency is "EUR". Sometimes you want to change only the currency without touching the other settings.
fun printReport(
title: String,
showList: Boolean = true,
showTotal: Boolean = true,
currency: String = "EUR"
) {
println("== $title ==")
println("showList=$showList, showTotal=$showTotal, currency=$currency")
}
fun main() {
printReport("Report") // == Report == / showList=true, showTotal=true, currency=EUR
printReport("Report", currency = "USD") // == Report == / showList=true, showTotal=true, currency=USD
}
If named arguments didn’t exist, we’d have to write something like printReport("Report", true, true, "USD")—and again guess what those two true values were.
Mixing positional and named arguments
In practice, you often end up with a “hybrid style”: the first 1–2 parameters are very clear by meaning (usually title, text, amount), so it’s convenient to pass them positionally; after that you get settings, and it’s better to pass them as named arguments. Kotlin supports this.
A common readability guideline: positional arguments first, then named ones. Such a call looks neat, like “main idea + refinements”.
Let’s apply this to our header:
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printHeader("Wallet", border = '#') // title first, then a setting
printHeader("Wallet", width = 10, border = '*')
}
This style is especially useful when you design a “mini-API” for yourself: the base parameters read quickly, and rare settings don’t get in the way—but are always available.
4. Practice: the “Wallet” menu
Mini-refactor: making the menu extensible
Now let’s assemble a small but cohesive piece of our console application “Wallet”. We are not adding anything fundamentally new in logic; we are improving the function interface: adding default parameters and using named arguments so that main() reads like a script.
We’ll have a standard menu: show commands, add an expense, print the list, print a report. We’re not doing “perfect input parsing” yet (that will come in later levels), so let’s keep it as simple line reading and simple checks.
fun printMenu(appName: String = "Wallet", showHint: Boolean = true) {
printHeader(appName, width = 25)
println("1) Add expense")
println("2) Show expenses")
println("3) Show total")
println("0) Exit")
if (showHint) println("Enter the command number and press Enter") // Enter the command number...
}
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printMenu() // default name and hint
printMenu(showHint = false) // same screen, but without the hint
printMenu(appName = "Budget v0") // custom name
}
The key idea here is: we made the default behavior “typical” for most calls, and moved rare tweaks into parameters. And thanks to named arguments, printMenu(showHint = false) reads like a clear phrase.
Cheat sheet: defaults and naming
At this point you usually want to ask: “OK, but how do I know what’s better: a default or naming?” The good news is: there are practical criteria. Defaults are a way to make the “common path” shorter, and named arguments are a way to make the “uncommon path” safer and clearer.
| Situation | What to use | Why |
|---|---|---|
| The parameter is almost always the same | Default value | Remove noise from most calls |
| Several parameters of the same type (Int, Boolean, Char) | Named arguments | Reduce the chance of mixing up the order |
| You need to change a “setting at the end” without touching the rest | Defaults + naming the needed setting | No need to pass “extra” arguments |
| The call reads poorly without decoding | Named arguments | The call becomes self-documenting |
And yes, sometimes the correct answer is: “the function is overloaded with parameters and needs to be simplified.” But that’s a sense you’ll develop a bit later, when there are many functions.
Decision diagram: should a parameter have a default
To solidify the approach, here’s a simple flowchart you can use to make the decision in your projects:
flowchart TD
A[There is a function with parameter P] --> B{Is P always required?}
B -->|Yes| C[Leave it without a default]
B -->|No| D{Is there a safe expected value?}
D -->|Yes| E[Make a default: P: Type = ...]
D -->|No| F[Keep it required, but call it with naming]
E --> G{Same-type parameters and easy to mix up?}
F --> G
G -->|Yes| H[Recommend named arguments]
G -->|No| I[Positional is OK if readable]
The word “safe” is key here. If a default can lead to weird behavior (for example, a default month = 0—but our months start at 1), it’s better to require the value explicitly.
5. Common mistakes
Mistake #1: a “just in case” default that looks convenient but breaks the meaning.
Sometimes you want to set a default just to make a parameter optional. For example, setting day: Int = 0. But if 0 is not a valid day, you’re planting a time bomb: the program will run, but the data will become garbage. A good default is not “any value”, but an expected and safe one.
Mistake #2: required parameters come after defaulted ones, and calls turn into riddles.
Such a design forces you to use named arguments all the time even where you wouldn’t want to. Kotlin explicitly shows this situation: if a defaulted parameter comes before a required one, you can omit it only via a named call. In learning projects it’s almost always easier to just reorder the parameters.
Mistake #3: several same-type arguments are passed positionally, and the order gets mixed up.
Three numbers in a row or three boolean flags in a row look the same, and an error in one place is very hard to notice with your eyes. In such calls, named arguments are not “chattiness”, but insurance.
Mistake #4: chaotic mixing of positional and named arguments.
When some arguments are positional, some are named, and then some are positional again (or just in a weird order), reading becomes painful. Kotlin allows named arguments in any order, but the goal isn’t to surprise a colleague—it’s to make the code obvious: the main thing first, then settings.
Mistake #5: too many parameters with defaults as an attempt to replace proper design.
If a function has 8 parameters, 6 of which have defaults, and you yourself can’t remember what each one means—that’s a signal the function is doing too much. In this course we won’t go into advanced constructs yet, but at minimum it’s worth stopping and asking: “Can I split this action into two simpler functions?”
GO TO FULL VERSION