1. Why input in main turns into a bedsheet of code
When you’re just starting out, everything looks harmless: in main, a couple of print()s, a couple of readln()s, and you already have age, height, and a favorite number. But 15 minutes later, suddenly your main looks like a 40-line accounting report: everything is about input, and almost nothing is about the program’s actual intent. And it’s not because you’re a bad programmer — it’s a normal stage of growth.
Imagine we’re making a simple console “profile questionnaire.” Naive code usually looks like this:
fun main() {
print("Name: ")
val name = readln()
print("Age: ")
val ageStr = readln()
val age = ageStr.toIntOrNull()
if (age == null) {
println("Age was entered incorrectly.")
return
}
println("Hi, $name! You are $age years old.")
// Hi, Alex! You are 20 years old.
}
Does it work? Yes. Is it convenient to extend? Not really. Because as soon as you add “height,” “weight,” “zip code,” and a couple more fields, you’ll start copying the same template by hand. And copy-paste is like wet snow: at first it’s “no big deal,” and then suddenly you slip and faceplant into a bug.
In Kotlin (and in programming in general), the typical reaction to repetition is to extract the repeated chunk into a function.
2. Prompt and the readLine(prompt) utility
prompt is a parameter, not a string sitting next to the code
The word prompt here means “a hint shown to the user before input.” It’s literally the text you print: "Age: ", "Enter width: ", and so on. It seems like a minor detail, but that detail is responsible for two important qualities of a program: user convenience and code readability.
Right now our prompt almost always looks like a pair of lines: print("..."), then readln(). And that creates duplication. If we learn to pass the prompt into a function as a parameter, we can write shorter and cleaner: “call the function, and it will show the prompt and read the input.” No magic — just discipline: hide repeating code inside a function.
It’s important to lock in this idea: an input utility should do exactly one job — help you obtain a value. It should not “decide the program’s fate,” choose a scenario branch, print long error lectures, and so on. That belongs in main (or other orchestration code), where the context is clearer.
readLine(prompt) — read a line with a prompt
Let’s start with the simplest (and very useful) function: print the prompt, read a line, return the line. Super simple — but it already removes copy-paste.
fun readLine(prompt: String): String {
print(prompt)
return readln()
}
fun main() {
val city = readLine("City: ")
println("You entered city: $city") // You entered city: Boston
}
Notice an important point: the function is called readLine, even though Kotlin has its own readLine() (which used to return nullable). Here we use readln(), a modern variant that returns String and assumes there will actually be input (for a learning console, that’s fine). In our tasks, “the user entered nothing at all and stdin ended” is rare, so we won’t complicate things yet.
And one more thing: for now we do not do trim(), do not normalize whitespace, and do not change letter case. That’s intentional. Input like " Bob " will be returned as-is for now. We’ll leave it as a “small pain” that we’ll fix in the next lecture when we talk about clean parsing and normalization.
3. Safe parsing and ...OrNull functions
readIntOrNull(prompt): Int?
Now let’s add the most common case: the user needs to enter an integer. The key idea: not toInt(), because toInt() will throw an exception on an invalid format and crash the program. What we want right now is predictable behavior: invalid input → null.
Let’s make a function with an honest name: readIntOrNull. The OrNull suffix is like a road sign that says “careful, there may be null here.” The calling code must handle it.
fun readIntOrNull(prompt: String): Int? {
print(prompt)
val s = readln()
return s.toIntOrNull()
}
fun main() {
val age = readIntOrNull("Age: ")
println("age = $age") // age = 20 (or age = null)
}
Why this is convenient:
Input code became compact and, more importantly, uniform. If you have 10 fields, you’ll call readIntOrNull(...) 10 times, instead of writing the same print/readln/toIntOrNull chain 10 times. And if you decide to improve input (for example, add trim()), you’ll fix it in one place.
A small “gotcha” that’s useful to know up front: if the user enters " 10 " (with spaces), toIntOrNull() will return null. And that’s fine for this lecture: we’re building the basic brick. In the next lecture we’ll separately discuss input normalization — and that’s where we’ll add trim() as a systematic solution.
readDoubleOrNull(prompt): Double?
In console tasks, Double appears no less often than Int: price, height, weight, average score, coordinates, “how many hours you slept.” And here too we want safe input so that "3,14" or "three.four" doesn’t turn the program into a pumpkin.
The structure is exactly the same — and that’s good: it’s easier for your brain to remember when functions look alike.
fun readDoubleOrNull(prompt: String): Double? {
print(prompt)
val s = readln()
return s.toDoubleOrNull()
}
fun main() {
val height = readDoubleOrNull("Height (in meters): ")
println("height = $height") // height = 1.82 (or height = null)
}
A real-life note: many users enter decimals with a comma ("1,82"). toDoubleOrNull() does not understand that because it expects a dot. We’re not solving this head-on yet, because that’s already “normalization and rules” — the next step. Our goal for now: don’t crash and honestly return null.
Flow: read → parse → return T?
So this sticks not as a set of spells but as a clear process, let’s draw a mini flowchart. It’s the same for Int, Double, and any other types you’ll parse later.
flowchart TD
A["Function readXOrNull(prompt)"] --> B["print(prompt)"]
B --> C["raw = readln()"]
C --> D[attempt parse raw]
D --> E{did it work?}
E -- yes --> F[return value T]
E -- no --> G[return null]
The point is that the function must not crash and must not make decisions for the scenario. It only converts a string into a value if possible.
The Kotlin approach with such helper functions for input is very common: you can find similar readInt()/readStr() even in practical examples for problems, just to reduce boilerplate input code.
toInt() vs toIntOrNull(), and friends
Beginners very often confuse “safe parsing” and “regular parsing.” Let’s lay it out clearly so there are no surprises later.
| What we do | Method | What it returns for "123" | What happens for "12a" |
|---|---|---|---|
| Parse Int “directly” | |
|
exception (program crashes) |
| Parse Int safely | |
|
|
| Parse Double “directly” | |
|
exception |
| Parse Double safely | |
|
|
That’s why for user input we almost always start with ...OrNull: input is “dirty,” and that’s normal. We don’t have to take the user’s word for it, especially if they’re typing at 2 a.m.
4. How to write scenario code with T?
Elvis and early exit
Having Int? and Double? means your scenario must be able to live with null. And here two extremes often appear: either someone starts writing huge nested ifs, or they use !! (and the program crashes again, just with a sense of guilt).
Let’s use a readable pattern: “if it didn’t work — say it and exit.” For this, the Elvis operator ?: works great together with run { ... }, where we can execute a couple of lines and do return.
fun readIntOrNull(prompt: String): Int? {
print(prompt)
return readln().toIntOrNull()
}
fun main() {
val age = readIntOrNull("Age: ") ?: run {
println("Age must be an integer.")
return
}
println("OK, age accepted: $age") // OK, age accepted: 20
}
Why this is nice:
The “happy path” code doesn’t drown in if (age != null) { ... }. We’re basically saying: “get the age, and if it didn’t work — here’s the reaction and we exit.” It’s a very human style: first the safety check, then the main logic.
Mini app: profile questionnaire
So we have a shared code story, let’s package a small scenario: ask for name, age, and height. We’re not doing complex rules yet (ranges, “not empty,” re-prompting) — that will come later. Today we practice one thing: compress the input template into small functions.
Here is a compact version of the program at the current knowledge level:
fun readLine(prompt: String): String {
print(prompt)
return readln()
}
fun readIntOrNull(prompt: String): Int? {
print(prompt)
return readln().toIntOrNull()
}
fun readDoubleOrNull(prompt: String): Double? {
print(prompt)
return readln().toDoubleOrNull()
}
fun main() {
val name = readLine("Name: ")
val age = readIntOrNull("Age: ") ?: run {
println("Age must be an integer.")
return
}
val height = readDoubleOrNull("Height (m): ") ?: run {
println("Height must be a number (for example 1.82).")
return
}
println("Profile: $name, $age years old, height $height m")
// Profile: Alex, 20 years old, height 1.82 m
}
Notice: we don’t argue with the user, and we don’t try to “guess” what they meant. If the input is invalid, we honestly say so and exit. That’s a normal policy at the beginning. Later we’ll learn to make programs more “patient” and ask the user to enter it again, but we won’t mix everything together all at once.
5. Common mistakes
Mistake #1: using toInt() / toDouble() for user input.
It looks simpler, but in practice any invalid character becomes a program crash. Today we practice the opposite style: “input error is an expected situation,” so toIntOrNull() and toDoubleOrNull() give you more stable control.
Mistake #2: naming the function readInt even though it returns Int?.
A name is a promise. If you name it readInt, a person (including you a week later) expects Int, not “maybe null, maybe not null.” The OrNull suffix is not bureaucracy; it’s a way to make the code self-documenting: you see the name and immediately understand the risk.
Mistake #3: printing long error messages inside the utility.
It’s very tempting to make readIntOrNull itself print “You entered nonsense!”, “Try again!”, “I’m disappointed in you!”. But then the utility stops being universal: in one place you ask for age, in another for width, in a third for number of attempts — and the message is still the same and out of place. Better: the utility returns null, and the scenario decides what exactly to tell the user.
Mistake #4: trying to fix all input problems inside the first version of the function.
For example, adding trim(), replacing a comma with a dot, allowing an empty string as 0, and five more “smart” heuristics. This makes the function unpredictable: today it “guessed,” tomorrow it “guessed wrong.” We move in small steps: first the basic brick, then normalization (next lecture), then rule validation (later).
Mistake #5: turning main into a forest of nested ifs.
When you get an Int?, your hand reaches for if (x != null) { if (y != null) { if (z != null) { ... }}}. It’s hard to read. A cleaner style at our level is Elvis ?: + run { ...; return }, to end the scenario quickly on input error and keep the “happy path” flat and understandable.
GO TO FULL VERSION