Kotlin / Language foundations and explicit state / Lesson 2
Express decisions and reusable functions
Use if and when expressions, ranges, loops, named arguments, and explicit function contracts.
On this page
Lesson goals
- Choose complete branch results and understandable function boundaries.
- Distinguish a range boundary, default argument, and side effect.
A branch can produce a value
Kotlin if and when constructs can be expressions. When used to initialize a value, every possible path must supply an appropriate result. This encourages code that names a decision instead of assigning a mutable temporary in several branches. Keep complex work out of a dense expression when it harms readability. A function with an expression body can infer its return type, while public or important boundaries often benefit from an explicit type that documents intent. Concision should make the policy visible rather than hide several decisions on one line.
Order predicates according to the rule
A when without a subject selects the first condition that evaluates to true. Overlapping predicates therefore require deliberate ordering. In the example, negative capacity is invalid, zero is full, and positive values are available. The final else covers all remaining integers. If a broader condition such as remaining >= 0 came first, the full state could become unreachable. Explain boundaries using zero, one, and a negative value before adding many ordinary cases. Boundary examples reveal mistaken inequalities that a single happy-path run can miss.
fun capacityLabel(remaining: Int): String = when {
remaining < 0 -> "invalid"
remaining == 0 -> "full"
else -> "available"
}
fun main() {
for (value in listOf(-1, 0, 1)) {
println(capacityLabel(value))
}
}
invalid
full
available
Expected output is invalid, full, available in that order. The function does not mutate its argument or consult any booking service. The program is unexecuted here because Kotlin tooling is unavailable.
Make iteration boundaries explicit
A closed range such as 1..3 includes both ends. A half-open range such as 0 until size excludes size and is useful for valid indices, although direct element iteration is clearer when the index is unnecessary. downTo and step express descending or spaced progressions. Avoid manually incrementing an index unless the algorithm needs that state. Break exits the nearest loop, while continue moves to its next iteration. Nested loops deserve especially clear naming or restructuring because a misplaced exit can silently skip required work.
Use named and default arguments to clarify a call
A default argument supplies a value when the caller omits that parameter; it is not a hidden global setting. Named arguments can make adjacent values with the same type easier to distinguish. The sample function computes a local label and has no external side effect. Avoid Boolean parameters whose meaning cannot be read at the call site without opening the declaration. Several modes may deserve an enum or separate function. Return Unit when no meaningful value is produced, and do not confuse a printed value with a returned one.
fun badge(name: String, prefix: String = "Guest"): String {
return "$prefix: $name"
}
fun main() {
println(badge("Mira"))
println(badge(name = "Salim", prefix = "Member"))
println((1..3).sum())
}
Guest: Mira
Member: Salim
6
Expected lines are Guest: Mira, Member: Salim, and 6. The first call uses the declared prefix, the second overrides it, and the range includes three. No runtime execution is claimed.
Keep input assumptions near the function boundary
A function signature tells the compiler about types, but it does not automatically express every domain restriction. An Int might still be negative when a seat count must be nonnegative. Decide whether to reject, normalize, or represent invalid input explicitly, and make callers aware of that choice. Keep pure calculation separate from printing or input so policy can be checked with fixed values. Later lessons introduce require, nullable results, and sealed outcomes; choose among them according to whether invalid input is a programming misuse or an expected external condition.
Common errors and fixes
The last valid item is skipped.
- Why it happens
- A half-open boundary was used where an inclusive range was intended.
- How to fix it
- Write the intended included values and choose the matching range.
- Prevent it next time
- Check empty, single-item, and final-boundary examples.
A broad when condition hides a specific branch.
- Why it happens
- The first matching condition wins.
- How to fix it
- Order conditions or make predicates mutually exclusive.
- Prevent it next time
- Use boundary cases for every neighboring predicate.
A caller receives Unit instead of the intended number.
- Why it happens
- The function printed the result but did not return it.
- How to fix it
- Return the calculation and print at the caller boundary.
- Prevent it next time
- Declare important return types explicitly.
Check your understanding
1. How would you classify zero as full before positive capacity?
Show worked solution
Use a zero-specific branch followed by a positive branch and an explicit invalid branch for negatives, or use the ordering shown in the example. Check -1, 0, and 1. The exact syntax is less important than complete and non-overlapping meaning at the boundaries.
2. Rewrite a function that only prints the sum so another function can reuse it.
Show worked solution
Define fun sum(a: Int, b: Int): Int = a + b, then call println(sum(2, 3)) at the presentation boundary. The reusable function now produces data rather than a console effect. Range and overflow assumptions still belong to the calculationโs contract; returning a value does not remove them.
Key takeaways
- Expression branches and explicit boundaries make decisions reviewable.
- A function should clearly distinguish returned data from side effects.