10. Missing values and input
In the last few lessons you’ve met some values that might be missing: the
first item of a list that could be empty, a dictionary lookup for a key
that might not be there, a number read from text that might not be a
number. Each time, you used ?? default to get past them.
This lesson explains what’s really going on, and gives you the full set of tools for missing values. Then you’ll use them for something new: programs that ask the user questions and read their answers.
In this lesson you’ll learn:
- what
nilmeans, and what an optional type likeInt?is - why Tessel makes you deal with missing values
- how to unwrap an optional with
if let,??and?. - how to read what the user types with
readLine() - how to turn typed text into numbers safely
- how to read all of the input at once, and how to report errors
What nil means
Section titled “What nil means”nil is Tessel’s word for “no value here”. It’s not zero, and it’s not an
empty string. Zero is a number, and "" is a piece of text with no
characters. nil means there’s no number and no text at all.
A variable that might hold nil has an optional type, written with a
? after the type. An Int? (say “optional Int”) is either an Int, or
nil:
fn main() { var middleName: String? = nil print(middleName == nil) middleName = "Augusta" print(middleName == nil)}truefalseOptionals are for values that are sometimes, legitimately, not there. Not everyone has a middle name. A shop might not have a price for an item. A user might type “hello” when you asked for a number.
You’ve already seen where optionals come from:
| Expression | Type | It’s nil when… |
|---|---|---|
list.first, list.last | Int? for an [Int] | the list is empty |
dict[key] | Int? for a [String: Int] | the key isn’t in the dictionary |
Int(text), Float(text) | Int?, Float? | the text isn’t a number |
readLine() | String? | there’s no more input (you’ll see this below) |
Common mistake: nil in a plain type
Section titled “Common mistake: nil in a plain type”Only optional types can hold nil. A plain Int always has a number in it:
fn main() { var score: Int = nil}error: `nil` can't be used as `Int` --> main.tsl:2:22 |2 | var score: Int = nil | ^^^ expected `Int` | = help: only optional values can be `nil`; make the type optional: `Int?`
1 error foundWhy Tessel makes you handle missing values
Section titled “Why Tessel makes you handle missing values”In many programming languages, any value can secretly be “nothing”. If your program then tries to use it (to count its letters, say), it crashes while running. These crashes are among the most common bugs in software.
Tessel avoids this. A plain String is always a real string. Only a
String? can be nil, and Tessel won’t let you use a String? as if it
were a String, until you’ve said what should happen when it’s nil:
fn main() { let nickname: String? = "Ada" print(nickname.count)}error: this might be `nil` --> main.tsl:3:11 |3 | print(nickname.count) | ^^^^^^^^ optional value | = help: use `?.count` to use it only when there is a value, or unwrap it with `if let`
1 error foundThis can feel fussy at first. But it means the mistake is found when you check your program, not by a user when it crashes. Getting the value out of an optional is called unwrapping it, as if the value were inside a box that might be empty. There are three ways to do it.
if let: run code only when there’s a value
Section titled “if let: run code only when there’s a value”if let checks whether an optional has a value. If it does, it gives the
value a name, and runs the block. Inside the block, the name is a plain,
non-optional value:
fn main() { let prices = ["coffee": 3, "tea": 2] if let price = prices["tea"] { print("Tea costs {price}. Two cost {price * 2}.") } else { print("We don't sell tea.") }}Tea costs 2. Two cost 4.Read if let price = prices["tea"] as “if there is a price for tea, call it
price, and…”. Inside the braces, price is an Int, so price * 2 works.
The else part runs when the value is nil. You can leave it out if
there’s nothing to do in that case.
It’s common to reuse the same name, which reads naturally:
fn main() { let nickname: String? = "Ada" if let nickname = nickname { print("{nickname} has {nickname.count} letters") }}Ada has 3 lettersCommon mistake: an optional as the condition
Section titled “Common mistake: an optional as the condition”Some languages let you write if price { … } to mean “if there is a price”.
In Tessel, a condition must be a Bool, and an optional isn’t one:
fn main() { let prices = ["coffee": 3, "tea": 2] let price = prices["tea"] if price { print("We sell tea.") }}error: a condition must be `true` or `false`, but this is `Int?` --> main.tsl:4:8 |4 | if price { | ^^^^^ | = help: to check that it has a value, write `x != nil`; to use the value, `if let value = x { … }`
1 error foundThe help gives you both choices, with x standing for your value. To only
check, write if price != nil. To use the price inside the block, write
if let price = price.
??: use a default
Section titled “??: use a default”You know this one already. a ?? b gives the value inside a if there is
one, and b if not. The result is a plain value:
fn main() { let settings = ["volume": 7] let volume = settings["volume"] ?? 5 let brightness = settings["brightness"] ?? 10 print("volume {volume}, brightness {brightness}")}volume 7, brightness 10Use ?? when there’s a sensible default. Use if let when you need to do
something different in the missing case, like showing a message.
?.: use it only if it’s there
Section titled “?.: use it only if it’s there”?. calls a method or reads a property only when there’s a value. If
there’s nil, the whole thing is nil. So the result is always optional,
and you’ll often finish with ??:
fn main() { let words = ["tessel", "is", "fun"] let empty: [String] = [] print(words.first?.uppercase() ?? "nothing") print(empty.first?.uppercase() ?? "nothing")}TESSELnothingwords.first is "tessel", so ?.uppercase() runs. empty.first is nil,
so ?.uppercase() is skipped, the result is nil, and ?? gives
"nothing".
Your own functions can return optionals
Section titled “Your own functions can return optionals”When a function might not have an answer, give it an optional return type.
Return nil for “no answer”:
fn indexOfFirstNegative(_ numbers: [Int]) -> Int? { for i in 0..numbers.count { if numbers[i] < 0 { return i } } nil}
fn main() { if let i = indexOfFirstNegative([4, 9, -2, 7]) { print("The first negative number is at index {i}") } print(indexOfFirstNegative([1, 2, 3]) ?? -1)}The first negative number is at index 2-1Whoever calls the function can’t forget that there might be no answer: Tessel makes them unwrap it.
Reading input with readLine()
Section titled “Reading input with readLine()”Until now, your programs have always done the same thing each time. Now
let’s make them ask questions. readLine() waits for the user to type a
line and press Enter, and gives you what they typed, without the Enter.
fn main() { print("What's your name? ", terminator: "") let name = readLine() ?? "stranger" print("Hello, {name}!")}Run it with tessel run hello.tsl, and the program waits for you to type.
Here’s what it looks like in the terminal, after typing Ada and pressing
Enter:
What's your name? AdaHello, Ada!Two new things here:
print(…, terminator: "")prints without going to a new line at the end. (Theterminatoris whatprintputs after the text, and it’s normally a line break.) That’s how you make a prompt: the cursor waits on the same line, right after the question.readLine()gives aString?, not aString. Why could it benil? Because the input can end. When there’s nothing more to read, you getnil. In a terminal you can end the input by pressing Ctrl+D.
Giving a program its input in advance
Section titled “Giving a program its input in advance”Typing the answers every time you test a program gets tedious. Instead, you
can send text into the program with a pipe, the | character in the
terminal. The text on the left becomes the program’s input:
fn main() { print("What's your name? ", terminator: "") let name = readLine() ?? "stranger" print("Hello, {name}!")}printf 'Grace\n' | tessel run hello.tslWhat's your name? Hello, Grace!printf prints its text, and \n in it is a line break, so this sends the
line Grace to the program. Notice that “Grace” doesn’t appear after the
prompt this time: when you type, the terminal shows your typing, but piped
input isn’t shown.
If there’s no input at all, readLine() gives nil, and ?? steps in:
printf '' | tessel run hello.tslWhat's your name? Hello, stranger!Reading every line with while let
Section titled “Reading every line with while let”while let is like if let, but it repeats: it keeps running the loop as
long as there’s a value, and stops at the first nil. With readLine(),
that means “read lines until the input ends”:
fn main() { var count = 0 while let line = readLine() { count += 1 print("{count}: {line.uppercase()}") } print("That was {count} lines.")}printf 'apples\npears\nplums\n' | tessel run shout.tsl1: APPLES2: PEARS3: PLUMSThat was 3 lines.Turning input into numbers
Section titled “Turning input into numbers”What the user types is always text. To do sums with it, convert it with
Int(…). Since the user might type something that isn’t a number, that
gives an optional too, and if let is the natural way to handle both
cases:
fn main() { print("What year were you born? ", terminator: "") let text = readLine() ?? "" if let year = Int(text.trim()) { print("In 2030 you'll be {2030 - year}.") } else { print("\"{text}\" isn't a year I understand.") }}In the terminal, typing 1990 and then, in a second run, nineteen:
What year were you born? 1990In 2030 you'll be 40.What year were you born? nineteen"nineteen" isn't a year I understand.trim() (from lesson 8) removes any spaces the user typed around the
number.
Common mistake: using the input as a number directly
Section titled “Common mistake: using the input as a number directly”fn main() { let text = readLine() ?? "" let year: Int = Int(text) print(2030 - year)}error: expected `Int`, found `Int?` --> main.tsl:3:21 |3 | let year: Int = Int(text) | ^^^^^^^^^ this is `Int?` | = help: this value might be `nil`; unwrap it with `if let`, or give a default with `?? …`
1 error foundReading all the input at once
Section titled “Reading all the input at once”Sometimes you want all of the input in one go, for example to count the
words in a whole file. readInput() reads everything until the input ends,
and gives it to you as one String. (It never gives nil: with no input,
it’s just "".)
fn main() { let text = readInput() let lines = text.lines() let words = text.words() print("{lines.count} lines, {words.count} words, {text.count} characters")}You can pipe a file into it with cat, or some text with printf:
printf 'the quick brown fox\njumps over\nthe lazy dog\n' | tessel run count.tsl3 lines, 9 words, 44 charactersReporting errors: printError and exit
Section titled “Reporting errors: printError and exit”When a program can’t do its job, it should say so clearly, and stop. Two built-in functions help:
printError(…)works likeprint, but writes to the standard error stream. That’s a separate channel for error messages, so they don’t get mixed up with the program’s real output when that’s piped into a file or another program. In the terminal, both appear on the screen as usual.exit(code)stops the program right away. The exit code is a number that tells whoever ran the program how it went:0means success, anything else means something went wrong.
fn main() { var total = 0 while let line = readLine() { if let n = Int(line) { total += n } else { printError("error: \"{line}\" is not a number") exit(1) } } print("Total: {total}")}printf '12\nabc\n5\n' | tessel run total.tslerror: "abc" is not a numberThe program stopped at the bad line, so “Total” was never printed. With good input, it runs to the end:
printf '12\n30\n5\n' | tessel run total.tslTotal: 47Worked example: an adding calculator
Section titled “Worked example: an adding calculator”Let’s write a calculator that asks for two numbers and adds them. It should be friendly when the user makes a typo: instead of stopping, it asks again. And if the input runs out, it should report an error.
The work of asking for a number is the same both times, so it goes in a
function. It returns an Int?: the number, or nil if the input ran out
before the user gave a valid number.
fn askNumber(_ prompt: String) -> Int? { print(prompt, terminator: "") while let line = readLine() { if let n = Int(line) { return n } print("\"{line}\" isn't a whole number. Try again: ", terminator: "") } nil}
fn main() { if let a = askNumber("First number: ") { if let b = askNumber("Second number: ") { print("{a} + {b} = {a + b}") return } } printError("error: I need two numbers") exit(1)}Here’s a run in the terminal, with a typo on the first try:
First number: five"five" isn't a whole number. Try again: 5Second number: 75 + 7 = 12And with piped input:
printf '5\n7\n' | tessel run add.tslFirst number: Second number: 5 + 7 = 12With only one number piped in, the input runs out, askNumber returns
nil, and the program reports the problem:
printf '5\n' | tessel run add.tslFirst number: Second number: error: I need two numbersHow it works:
askNumberprints the prompt, then reads lines until one is a whole number, and returns it. Thewhile letloop only ends if the input runs out, and then the function’s last line,nil, is the result.- In
main, twoif lets, one inside the other, unwrap both numbers. Only if both are there do we print the sum. The plainreturnthen endsmain, so the error lines below are skipped. - If either is
nil, we fall through toprintErrorandexit(1).
Exercises
Section titled “Exercises”1. Default greeting. Declare let name: String? = nil. Print
Hello, <name>!, using "friend" when the name is missing. Then change
nil to "Ada" and run it again.
Solution
fn main() { let name: String? = nil print("Hello, {name ?? "friend"}!")}Hello, friend!With let name: String? = "Ada", it prints Hello, Ada!.
2. Safe division. Write fn divide(_ a: Int, by: Int) -> Int? that
returns nil when by is 0 (instead of stopping the program with a
division-by-zero error), and a / by otherwise. Use if let to print
the result of divide(10, by: 2), and ?? to print divide(1, by: 0)
with a default of 0.
Solution
fn divide(_ a: Int, by: Int) -> Int? { if by == 0 { return nil } a / by}
fn main() { if let result = divide(10, by: 2) { print("10 / 2 = {result}") } print(divide(1, by: 0) ?? 0)}10 / 2 = 503. Longest line. Write a program that reads lines until the input ends,
and then prints the longest line and its length. Test it with
printf 'short\nthe longest line\nmedium one\n' | tessel run longest.tsl.
Solution
fn main() { var longest = "" while let line = readLine() { if line.count > longest.count { longest = line } } print("Longest: \"{longest}\" ({longest.count} characters)")}Longest: "the longest line" (16 characters)4. Average of the input. Read numbers, one per line, until the input
ends, and print their average with two decimals. Skip lines that aren’t
numbers, but print a warning about each one with printError. If there
were no numbers at all, print an error and exit with code 1. Test it with
printf '4\n8\noops\n9\n' | tessel run average.tsl.
Solution
fn main() { var numbers: [Int] = [] while let line = readLine() { if let n = Int(line) { numbers.append(n) } else { printError("skipping \"{line}\"") } } if numbers.isEmpty { printError("error: no numbers given") exit(1) } let average = Float(numbers.sum()) / Float(numbers.count) print("Average of {numbers.count} numbers: {average.formatted(decimals: 2)}")}skipping "oops"Average of 3 numbers: 7.005. Guess the number. Write a game: the program picks a secret number from
1 to 100 with random(1..101) (which gives a random Int from 1 up to,
but not including, 101). The player guesses, and after each guess the
program says “Too low” or “Too high”, until the guess is right. Then it
says how many guesses it took. If the input ends first, reveal the
number. If the player types something that isn’t a number, ask again
without counting it as a guess.
Solution
fn main() { let secret = random(1..101) var guesses = 0 print("I'm thinking of a number from 1 to 100.") print("Your guess: ", terminator: "") while let line = readLine() { if let guess = Int(line.trim()) { guesses += 1 if guess < secret { print("Too low. Guess again: ", terminator: "") } else if guess > secret { print("Too high. Guess again: ", terminator: "") } else { print("Yes! It was {secret}. You got it in {guesses} guesses.") return } } else { print("Please type a whole number: ", terminator: "") } } print("") print("The number was {secret}. Bye!")}A game in the terminal (your secret number will be different):
I'm thinking of a number from 1 to 100.Your guess: 50Too low. Guess again: 75Too low. Guess again: 88Too low. Guess again: 94Too high. Guess again: 91Too high. Guess again: 89Yes! It was 89. You got it in 6 guesses.The smart strategy is to always guess halfway between what you know is too low and what you know is too high. With it, you never need more than 7 guesses. This idea, called binary search, comes back in lesson 17.
Summary
Section titled “Summary”nilmeans “no value”. Only an optional type, likeInt?orString?, can holdnil.- Tessel won’t let you use an optional as a plain value until you’ve
handled the
nilcase. This catches a whole family of crashes before your program runs. if let x = optional { … } else { … }runs code only when there’s a value, and names it.optional ?? defaultgives the value, or the default.optional?.method()calls the method only when there’s a value; the result is optional.- Your own functions can return
T?andnilfor “no answer”. readLine()reads one line of input as aString?; it’snilwhen the input ends.while let line = readLine() { … }reads every line.print(…, terminator: "")prints a prompt without a line break.Int(text)turns input into a number, as anInt?.readInput()reads all the input at once.printError(…)reports errors, andexit(code)stops the program; a code other than0means failure.- Test programs that read input by piping text in:
printf '5\n7\n' | tessel run add.tsl.
For more, see Optionals and Errors in the language guide, and readLine, readInput and printError in the standard library reference.
Next: 11. Structs