JavaRush/Courses/Kotlin SELF/Designing mini utilities with vararg

Designing mini utilities with vararg

Open

1. A mini output library

When you write learning console programs, a strange effect shows up very quickly: 20% of the code solves the task, and the other 80% is “politely talking to the user.” Print a header, output a list, add indentation, show an error, format the result nicely. And if you do all of this in main, it turns into a 12-season TV series nobody asked for.

Mini utilities are small functions you write “for yourself” so your code reads like text. Ideally, main should look like a script: “read → validate → compute → print nicely.” And details like “how exactly to print nicely” are hidden inside utilities.

vararg is an almost perfect tool here, because printing and formatting often work with a set of strings: header + several lines + footer; or an error message made of several parts; or a list of items you want to pass naturally: printBlock("a", "b", "c").

printHeader(...): simple header printing

Now we’ll start assembling a mini set of functions for console output. This is not a “framework” or “architecture,” just neat tools. Imagine you’re making yourself a small Swiss Army knife: a blade, a screwdriver, and a corkscrew. The main thing is not to try to cram a microwave into it right away.

Let’s start with a function that prints a header. We want to call it like this:

printHeader("Budget Tracker", "version 0.1")

And get something like:

=== Budget Tracker ===
version 0.1

The implementation can be as straightforward as possible:

fun printHeader(title: String, vararg subtitleLines: String) {
    println("=== $title ===")
    for (line in subtitleLines) {
        println(line)
    }
}

fun main() {
    printHeader("Budget Tracker", "version 0.1", "commands: add/list/exit")
}

Notice two important design points.

First: vararg is at the end. That way the call reads naturally, and you don’t have to switch to “named-arguments mode” for every parameter after vararg. This is a direct consequence of vararg rules: if vararg isn’t last, then parameters after it are usually passed by name.

Second: an empty call is also valid:

fun main() {
    printHeader("Budget Tracker")
}

This is fine: a header without subtitles is a perfectly meaningful scenario.

printJoined(...): printing values into a single line

When you print a set of values, you often want to output them on one line with a separator: a, b, c or a | b | c. Kotlin has built-in functions for this, but in this course it’s useful to practice writing a “simple one of our own” to better feel strings and loops.

Let’s make a utility that:

  • takes vararg items: String
  • has “options”: separator, prefix, suffix
  • prints the result

Important: we place “options” after vararg. Then it’s logical to pass them as named arguments, so the call doesn’t look like “guess the parameter by position.”

fun printJoined(
    vararg items: String,
    separator: String = ", ",
    prefix: String = "",
    suffix: String = ""
) {
    val sb = StringBuilder()
    sb.append(prefix)

    var first = true
    for (item in items) {
        if (!first) sb.append(separator)
        sb.append(item)
        first = false
    }

    sb.append(suffix)
    println(sb.toString())
}

fun main() {
    printJoined("kotlin", "vararg", "spread")                 // kotlin, vararg, spread
    printJoined("a", "b", "c", separator = " | ")             // a | b | c
    printJoined("OK", prefix = "[", suffix = "]")             // [OK]
}

There’s a subtle design point here: separator, prefix, suffix are parameters that, semantically, are configuration—not part of the data. That’s why named arguments make the call “self-documenting”: the reader sees separator = " | ", and the brain doesn’t waste energy decoding it.

And one more point: if items is empty, the result will just be prefix + suffix. Sometimes that’s useful (for example, printing empty brackets []), sometimes it’s not. It’s important to remember: vararg almost always makes you think about f().

Cheat sheet: how to design readable calls

When you design a utility, the main criterion isn’t how “cleverly” you wrote the function, but how pleasant it is to call. So it’s useful to keep a simple cheat sheet in mind:

What we want at the call site So, in the signature
“Data” is listed: f("a", "b") vararg for data
“Settings” read explicitly: separator = ... parameters after vararg + default values
Don’t guess what “empty” means either a neutral result, or ...OrNull, or require
Easy to reuse one utility inside another forwarding vararg via *

The last point is the core of today’s lecture: forwarding vararg.

3. Forwarding vararg and utility chains

An array ≠ a set of arguments: the spread operator *

The most common situation in real code: you wrote one useful vararg function, and then you want to write a second one that does a bit more, but calls the first one internally.

For example:

  • printLines(vararg lines: String) — prints lines as-is
  • printBlock(title: String, vararg lines: String) — prints a title, then calls printLines(...) for the lines

Intuitively, you want to do this:

fun printLines(vararg lines: String) {
    for (line in lines) println(line)
}

fun printBlock(title: String, vararg lines: String) {
    println("== $title ==")
    printLines(lines) // ERROR
}

But lines inside printBlock is already an array (Array<String>), while printLines(...) expects a set of arguments. These are different call forms.

The correct way is the spread operator *, which “expands” an array into a list of individual arguments.

fun printLines(vararg lines: String) {
    for (line in lines) println(line)
}

fun printBlock(title: String, vararg lines: String) {
    println("== $title ==")
    printLines(*lines) // <-- expanded the array into vararg
}

fun main() {
    printBlock("Today", "coffee", "code", "more code")
}

If you internalize the idea “array ≠ set of arguments” once, 70% of weird vararg errors will disappear on their own.

Forwarding + adding your own argument: printBlock(...) with a footer

Sometimes you want to:

  • take a set of lines
  • add one more line to it (for example, a footer)
  • print everything as one block

You can do it like this:

fun printLines(vararg lines: String) {
    for (line in lines) println(line)
}

fun printBlock(title: String, vararg lines: String, footer: String = "") {
    println("== $title ==")

    if (footer.isEmpty()) {
        printLines(*lines)
    } else {
        // Mixed call: some arguments manually, some via *lines
        printLines(*lines, footer)
    }
}

fun main() {
    printBlock("Shopping", "bread", "milk", footer = "Total: 2 items")
}

A mixed call (*lines, footer) is a direct “super case” for vararg: it’s like you’re building one shared argument list from two sources.

And yes: footer comes after vararg, which means we pass it by name in the call printBlock(..., footer = ...). This is expected and matches the vararg rules.

Mini logger: log(tag, vararg messages) and wrappers info(...), error(...)

Let’s make a utility that prints messages with a “tag,” like in logs:

[INFO] started
[INFO] loading data
[ERROR] invalid input

Let the base function do one thing: print one line:

fun logLine(tag: String, message: String) {
    println("[$tag] $message")
}

fun main() {
    logLine("INFO", "started")  // [INFO] started
}

Now let’s make log(tag, vararg messages), which prints many lines but reuses logLine:

fun logLine(tag: String, message: String) {
    println("[$tag] $message")
}

fun log(tag: String, vararg messages: String) {
    for (m in messages) {
        logLine(tag, m)
    }
}

fun main() {
    log("INFO", "started", "loading data", "done")
}

And now let’s make “wrappers” info(...) and error(...), which fix the tag and just forward the messages further.

This is where forwarding vararg shows up in full:

fun log(tag: String, vararg messages: String) {
    for (m in messages) {
        println("[$tag] $m")
    }
}

fun info(vararg messages: String) {
    log("INFO", *messages)
}

fun error(vararg messages: String) {
    log("ERROR", *messages)
}

fun main() {
    info("app started", "user = guest")
    error("wrong input", "expected number")
}

If you forget *messages, the compiler will say the types don’t match: it will see an attempt to pass Array<String> where individual String values are expected. And here the compiler is your friend: it saves you from a logical bug. The spread operator is the officially supported way to “pass an array as vararg.”

Diagram: what a “utility chain” looks like in the console

Sometimes it’s useful to see this not only with your eyes, but also as a diagram—especially when there are several utilities.

flowchart TD
    A[main] --> B["info(vararg messages)"]
    B --> C["log(tag, vararg messages)"]
    C --> D["println(...)"]

The point of this diagram is simple: small functions don’t have to “do something new.” Sometimes their job is to lock in configuration (tag = "INFO") and make a call shorter and clearer.

4. Mini example: printing a report

Now let’s combine everything into a mini scenario similar to a real console program. Suppose we have “expense tracking” (a conditional one), and we want to nicely print a report of several lines.

Let’s make a utility printReport(title, vararg lines) that:

  • prints a header
  • prints the lines
  • prints a separator line
fun printSeparator(char: Char = '-', count: Int = 20) {
    repeat(count) { print(char) }
    println()
}

fun printReport(title: String, vararg lines: String) {
    printSeparator('=')
    println(title)
    printSeparator('=')
    for (line in lines) println(line)
    printSeparator()
}

fun main() {
    printReport(
        "Daily report",
        "coffee: 3.50",
        "lunch: 12.00",
        "total: 15.50"
    )
}

Here we use what you already know: repeat(...) and println. At the same time, the design of printReport is such that, at the call site, it reads almost like “report data.”

If you notice, printSeparator is also a small utility, but already without vararg. This is normal practice: vararg is good where it’s natural to pass “many same-typed pieces.”

5. More examples and anti-examples of vararg

When you should not use vararg

It’s very easy to fall in love with vararg and start doing this:

fun doEverything(vararg args: String) { ... }

…then suddenly realize you wrote a “bag function,” and nobody (including you a week later) knows what to pass into it or in what order.

For our utilities, a good rule is: vararg should be homogeneous in meaning. If these are report lines—great. If these are log messages—great. If this is “command, username, password, mode, timeout, and three more things”—that’s not vararg; that’s an attempt to hide a bad design under the rug.

A numbers example: printStats(...) and the ...OrNull contract

Let’s make one more utility where vararg is a set of numbers, and the function prints statistics. We won’t use collections or complex operations—only loops and variables, as you already can.

fun meanOrNull(vararg xs: Int): Double? {
    if (xs.size == 0) return null

    var sum = 0
    for (x in xs) sum += x
    return sum.toDouble() / xs.size
}

fun printStats(label: String, vararg xs: Int) {
    val mean = meanOrNull(*xs)
    println("$label: mean = ${mean ?: "n/a"}")
}

fun main() {
    printStats("Speeds", 10, 12, 11)     // Speeds: mean = 11.0
    printStats("Empty")                  // Empty: mean = n/a
}

Notice two things.

First, printStats receives xs as vararg, but passes it into meanOrNull via *xs. This is that same forwarding of an argument set again.

Second, meanOrNull is an example of the ...OrNull contract: if there’s no data, there’s no result. The calling code must handle null, and that’s honest.

6. Typical mistakes when designing vararg utilities

Mistake #1: the utility makes the call shorter, but the meaning more vague.
Sometimes a vararg function looks “cool” because you can pass anything into it. But if, at the call site, it’s unclear what strings you’re passing and why exactly that way, you’ve made readability worse, not better. A utility is good when the call looks like a sentence: “print a report with these lines,” “log these messages.”

Mistake #2: forgetting * when forwarding vararg into another vararg function.
This is the classic one: inside you have a parameter messages, and you write log("INFO", messages). But messages is an array, not an argument list. You need log("INFO", *messages), because the spread operator is the officially supported way to pass an array as vararg.

Mistake #3: too many “options” around vararg, and the call becomes like an airplane cockpit panel.
If a function has three or four parameters after vararg, and on top of that they have no default values, then every call turns into a long line with a bunch of name = value. Sometimes that’s fine, but more often it’s a sign you’re trying to cover too many scenarios with one function. It’s better to have two small utilities than one “universal” one—universality in learning code often means “now nobody understands it.”

Mistake #4: using vararg “just in case,” even though there are always exactly two values.
If, semantically, there are always two elements (for example, “login and password”—though you shouldn’t do that in real projects), then vararg will only make the design worse. vararg is useful when the number of values really varies, and that’s a normal part of the scenario.

Mistake #5: not thinking through the empty-set case, and the function crashes on xs[0].
This is especially common in “max” or “min” style functions: you take xs[0] as the starting value, and then someone suddenly calls max() with no arguments. If an empty set is allowed, return null (...OrNull) or a neutral value where it’s honest. If an empty set is a usage error, enforce the contract via require(...), so the failure is early and clear.

1
Task
Kotlin SELF,  16 level,  4 lecture
Locked
Application header
Application header
1
Task
Kotlin SELF,  16 level,  4 lecture
Locked
Status line
Status line
1
Task
Kotlin SELF,  16 level,  4 lecture
Locked
Printed block
Printed block
1
Task
Kotlin SELF,  16 level,  4 lecture
Locked
Level-based logs
Level-based logs
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet