Kotlin's if Returns a Value, So There's No Ternary


Java draws a hard line between expressions, which produce a value, and statements, which just do something. if is a statement, so to choose a value with it you either reach for the cramped ?: ternary or assign inside both branches. Kotlin erases the line: if is an expression that produces a value, which is exactly why Kotlin has no ternary operator — it would be redundant.

This is the third post in the series, picking up from functions. Here’s the everyday control flow you write inside them.

if is an expression

Used as a statement, if looks like Java:

if (score > 50) println("pass") else println("fail")

But it also returns the value of whichever branch runs, so you can assign it:

val grade = if (score > 50) "pass" else "fail"

The value of an if branch is its last expression, so the braces version works too:

val message = if (score > 90) {
    "excellent"
} else if (score > 50) {
    "pass"
} else {
    "fail"
}

This is why there’s no score > 50 ? "pass" : "fail" in Kotlin — the plain if already does that job, and reads better doing it. (An if used as an expression must have an else; otherwise the compiler can’t say what the value is when the condition is false.)

Ranges

Before loops, meet the range — a first-class value describing a span. You build one with ..:

val digits = 0..9        // 0 through 9, both ends included

A few variants cover the common cases:

1 until 10               // 1 to 9 — excludes the upper end
10 downTo 1              // counts downward
1..10 step 2             // 1, 3, 5, 7, 9

Ranges also answer membership questions with in, which reads like English and beats a pair of comparisons:

if (age in 18..65) { /* ... */ }
if (c in 'a'..'z') { /* ... */ }

for loops over anything iterable

There’s no C-style for (int i = 0; ...) in Kotlin. A for loop walks over something iterable — and a range is iterable:

for (i in 1..5) println(i)        // 1 2 3 4 5
for (i in 10 downTo 1 step 2) println(i)

The same loop walks a collection directly, no index needed:

for (name in names) println(name)

When you genuinely need the index, ask for it explicitly with withIndex rather than tracking a counter by hand:

for ((i, name) in names.withIndex()) {
    println("$i: $name")
}

while and do-while

These are the one place Kotlin keeps Java’s shape exactly, because there was nothing to improve:

while (queue.isNotEmpty()) {
    process(queue.removeFirst())
}

do {
    val line = readLine()
} while (line != null)

break and continue, with labels

break and continue work as you’d expect. The addition is labels, for when you need to break out of an outer loop from within an inner one — the situation that pushes Java code toward a flag variable or an early return:

outer@ for (row in grid) {
    for (cell in row) {
        if (cell.isMine) break@outer   // exits both loops
    }
}

A label is a name followed by @. You’ll reach for this rarely, but when you do, it’s far clearer than the alternatives.

Iterating by index when you must

withIndex() hands you the index and the element together. When you want the index by itself, loop over a range of the valid positions — no hand-managed counter required. until is the idiomatic choice, since it stops one short, exactly how array bounds work:

for (i in 0 until list.size) {
    println("$i -> ${list[i]}")
}

Collections expose an indices range directly, which reads better still:

for (i in names.indices) println(names[i])

Reach for these only when the index matters. Most loops want the elements, not their positions.

in checks membership, not just ranges

in isn’t limited to ranges — it checks membership in any collection too, which collapses a chain of || comparisons into one line:

if (command !in validCommands) {     // validCommands is a list
    return error("unknown command")
}

Loops don’t return values

One asymmetry to keep straight: if and when are expressions, but for and while are not. A loop does work; it doesn’t hand back a value, so there’s no val x = for (...). When you want to turn a loop into a result — a sum, a filtered list — that’s a job for the collection functions we’ll meet later, not the loop itself.

A worked example

Put the pieces together — if as an expression, a range check, and a for over a collection — and the logic stays flat, with no temporary flags propped up just to be read two lines down:

fun summarize(scores: List<Int>): String {
    var passed = 0
    for (score in scores) {
        val band = if (score >= 90) "A" else if (score >= 50) "pass" else "fail"
        if (band != "fail") passed++
    }
    return if (passed == scores.size) "all clear" else "$passed of ${scores.size} passed"
}

Final thoughts

Making if an expression isn’t a cosmetic change — it removes the ternary operator, shrinks the gap between “decide a value” and “do a thing,” and sets up a pattern you’ll see throughout Kotlin: constructs return values, so you compose them instead of assigning into pre-declared variables.

The same expression idea powers the construct you’ll actually reach for once a choice has more than two branches: when, which replaces switch, long if/else ladders, and instanceof in one move.

Practice: reinforce this with the companion workbook — short, click-to-reveal problems.