05: Control Flow
Audience: All
Time: 90 minutes
Prerequisites: 01-04
You'll learn: if/else, match, for loops, while loops, guards, break/continue
The Big Picture
Control flow directs which code runs based on conditions. Instead of executing every line in order, you:
- Skip code if a condition isn't met (
if) - Repeat code until done (
for,while) - Branch based on many options (
match)
If / Else
Basic If
# file: 05_if.zbr
# teaches: conditional execution
# chapter: 05-Control-Flow
def main()
var age = 18
if age >= 18
print("You can vote")
If / Else
# file: 05_if_else.zbr
# teaches: if-else branching
# chapter: 05-Control-Flow
def main()
var score = 75
if score >= 90
print("A")
else if score >= 80
print("B")
else if score >= 70
print("C")
else
print("F")
Breakdown:
if condition— Execute if trueelse if condition— Try the next condition (Zebra spells the chainelse if, two words)else— Default case (if all above are false)
Multiple Conditions
# file: 05_conditions.zbr
# teaches: boolean logic
# chapter: 05-Control-Flow
def main()
var age = 25
var has_license = true
if age >= 18 and has_license
print("Can drive")
if age < 16 or not has_license
print("Cannot drive")
Operators:
and— Both must be trueor— At least one must be truenot— Flip the value
Inline Conditions
Zebra lets you write short conditions on one line, and use if as an
expression that produces a value.
One-Line If Statement
# file: 05_inline_if.zbr
# teaches: one-line if statement
# chapter: 05-Control-Flow
def main()
var x = 10
if x > 0: print("positive") # colon required before body
# One-line with else:
var label = ""
if x > 0: label = "pos" else: label = "non-pos"
# Chained else-if on one line:
var grade = 78
var letter = ""
if grade >= 90: letter = "A" else if grade >= 80: letter = "B" else: letter = "C"
print(letter) # C
The : is required before the body. No block — the condition and body sit
on the same line.
If as an Expression
To produce a value — to initialise a variable or pass an argument — use the
three-argument form if(condition, value_if_true, value_if_false):
# file: 05_if_expr.zbr
# teaches: if-expression
# chapter: 05-Control-Flow
def main()
var score = 85
# Choose a value:
var verdict = if(score >= 60, "pass", "fail")
print(verdict) # pass
# Use it directly as an argument:
print(if(score > 90, "excellent", "good")) # good
# Absolute value:
var n: int = -5
var abs_n: int = if(n >= 0, n, -n)
print(abs_n) # 5
Rules for the expression form:
- It takes exactly three arguments, and the two values must have the same type.
- For more than two outcomes, nest it —
if(a, x, if(b, y, z))— or use the statement form above and assign inside each branch. - The colon form
if cond: a else: bis a statement only: it runs one of two statements, and it does not parse where a value is expected.
Coming from Python? This does the job of
value_if_true if cond else value_if_false, but reads condition-first, like a function call.
Match (Pattern Matching)
Match is more powerful than if/else. It handles many cases elegantly.
Simple Match
# file: 05_match.zbr
# teaches: pattern matching
# chapter: 05-Control-Flow
def main()
var color = "red"
branch color
on "red"
print("Stop")
on "yellow"
print("Caution")
on "green"
print("Go")
else
print("Unknown")
Match With Types
Zebra's idiomatic way to dispatch on "what kind of thing is this" is a
union plus branch. Each variant of the union carries its own data:
# file: 05_match_type.zbr
# teaches: type-based matching with unions
# chapter: 05-Control-Flow
union Pet
dog: str # name
cat: str # name
fish: str # tank-size
def main()
var pet: Pet = Pet.dog("Rex")
branch pet
on Pet.dog as name
print("${name} says: Woof!")
on Pet.cat as name
print("${name} says: Meow!")
on Pet.fish as size
print("tank: ${size}")
else
pass
Zebra has no class inheritance, so dispatching on a class hierarchy isn't the natural pattern here. See Chapter 09 (Composition and Mixins) for why, and Chapter 07b (Structs and Unions) for more on tagged unions.
If you know Python
# Python
match color:
case "red":
print("Stop")
case "green":
print("Go")
# Zebra
branch color
on "red"
print("Stop")
on "green"
print("Go")
Loops
For Loop (Iteration)
# file: 05_for.zbr
# teaches: for loop iteration
# chapter: 05-Control-Flow
def main()
var fruits = List(str)()
fruits.add("apple")
fruits.add("banana")
fruits.add("cherry")
for fruit in fruits
var f: str = fruit # typed local for {s} formatting
print(f)
# Numeric range — n.to(end) is exclusive on `end`.
var i = 0
while i < fruits.count()
print("Item ${i}: ${fruits.at(i)}")
i = i + 1
While Loop (Condition-Based)
# file: 05_while.zbr
# teaches: while loop
# chapter: 05-Control-Flow
def main()
var count = 0
while count < 5
print("Count: ${count}")
count = count + 1
print("Done!")
Break and Continue
# file: 05_break_continue.zbr
# teaches: loop control
# chapter: 05-Control-Flow
def main()
# Break: exit loop early
var i = 0
while true
if i == 5
break
print(i)
i = i + 1
# Continue: skip to next iteration. Numeric ranges use either `start:end`
# or the equivalent method form `start.to(end)` — both are exclusive on
# `end` (so `1.to(11)` yields 1,2,...,10).
for num in 1.to(11)
if num % 2 == 0
continue
print(num) # Prints odd numbers only
for walks lists, ranges and maps. It also walks any type of your own
that declares the iteration cues, cue iter / cue next; Chapter 07
("Cues") shows how. The quickest way to get such a type is to let the
compiler write it, with a generator.
Generators: Lazy Sequences with yield
A top-level def whose return type is Iter(T) is a generator. Its
body does not run when you call it. It runs a step at a time as the
caller asks for values: each yield v hands one value to the loop and
pauses, and the next request resumes the body just after the yield.
Nothing is collected into a list first.
# file: 05_generators.zbr
# teaches: generators -- a def returning Iter(T) that produces values with yield
# chapter: 05-Control-Flow
def evens(limit: int): Iter(int)
var i = 0
while i < limit
yield i
i = i + 2
def longWords(words: List(str), n: int): Iter(str)
for w in words
if w.len > n
yield w
def naturals(): Iter(int) # never finishes on its own
var n = 1
while true
yield n
n = n + 1
def main()
for x in evens(7)
print(x)
var words = ["generator", "is", "lazy", "iteration"]
for w in longWords(words, 4)
print(w)
for n in naturals() # the consumer decides when to stop
if n > 3
break
print("n = ${n}")
var it = evens(4) # next() by hand: one value per call
print(it.next()!)
print(it.next()!)
print(it.next() == nil) # finished
Output:
0
2
4
6
generator
iteration
n = 1
n = 2
n = 3
0
2
true
What happened here:
evens(7)produced 0, 2, 4, 6 one at a time; the loop'silives across the pauses.naturals()would run forever on its own. It is safe because the caller stops asking (break), and the generator never computes a value nobody asked for.- A generator is an iterator, so
it.next()gives the next value, andnilonce the body has finished.
The rules:
yield vproduces a value. A barereturnfinishes early; a generator cannotreturna value ("a generator finishes with a barereturn; it produces values withyield").Iter(T)is only a return type. Bind the call (var it = evens(4)) and let the type be inferred;var it: Iter(int) = ...is refused.- Generators are top-level
defs. A method cannotyield(yet). - A
yieldinsidebranch,try,with,usingorallocateis refused by name; restructure with anifchain. Aforthat yields can walk aList, a range, or another iterable (a generator included), with one loop variable and noiffilter on the loop.
Under the hood: the compiler turns a generator into a class with
cue next, the same shape you would write by hand in Chapter 07, with the function's locals as fields.
Guards
Guards are conditions that must be true for code to run. Zebra spells them with
the one early-exit form it has — an inline if — rather than a dedicated keyword (a
guard statement existed until 2026-09-15 and was removed: it was if not with a
second spelling).
# file: 05_guards.zbr
# teaches: guard conditions
# chapter: 05-Control-Flow
def process(name: str)
# Early returns state the precondition and leave at once:
if not name.len > 0: return
if name.len > 100: return
# Process only if all guards passed
print("Processing: ${name}")
def main()
process("") # Guard fails — returns early
process("Alice") # Guards pass — processes
process("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") # too long — returns early
if not cond: return reads as "the call must satisfy cond; otherwise return now". When the precondition is a contract — something the caller was wrong to violate rather than an input to tolerate — use require instead (Chapter 14): it names the violation and can be stripped from a --turbo build.
Real World: Data Validation
# file: 05_validation.zbr
# teaches: practical control flow
# chapter: 05-Control-Flow
def validate_email(email: str): bool
# Check not empty
if email.len == 0
return false
# Check has @
if not email.contains("@")
return false
# Check has domain
var parts: List(str) = email.split("@")
if parts.count() != 2
return false
var domain: str = parts.at(1)
if not domain.contains(".")
return false
return true
def main()
var emails = List(str)()
emails.add("alice@example.com")
emails.add("invalid.email")
emails.add("bob@domain.co")
for email in emails
var e: str = email
if validate_email(e)
print("Valid: ${e}")
else
print("Invalid: ${e}")
Common Patterns
Early Return
def process_order(order_id: int): bool
var order = fetch_order(order_id)
if order == nil
return false
if order.total == 0
return false
if not order.has_items
return false
# Do the real processing
charge_card(order)
ship_items(order)
return true
Nested Conditions
for user in users
if user.is_active
for order in user.orders
if order.status == "pending"
process_order(order)
Else-If Chain
if age < 13
category = "child"
else if age < 18
category = "teen"
else if age < 65
category = "adult"
else
category = "senior"
Common Mistakes
❌ Mistake: Forgetting the condition
if age >= 18 # Missing condition or wrong structure print("Adult") print("Extra") # Both print regardless✅ Better:
if age >= 18 print("Adult") else print("Minor")
❌ Mistake: Unreachable code
if x > 0 return true else return false return false # ❌ Never reached✅ Better:
if x > 0 return true return false
❌ Mistake: Mutating collection while iterating
for fruit in fruits if fruit == "apple" fruits.remove(0) # ❌ Unsafe — modifying during iteration✅ Better:
var to_remove = List(int)() # collect indices var idx = 0 for fruit in fruits if fruit == "apple" to_remove.add(idx) idx = idx + 1 # Remove in reverse so earlier indices stay valid. var k = to_remove.count() - 1 while k >= 0 fruits.remove(to_remove.at(k)) k = k - 1
Exercises
Exercise 1: Grade Calculator
Write a function that converts numeric scores to letter grades:
Solution
def grade_to_letter(score: int): str
if score >= 90
return "A"
else if score >= 80
return "B"
else if score >= 70
return "C"
else if score >= 60
return "D"
else
return "F"
def main()
var scores = List(int)()
scores.add(95)
scores.add(75)
scores.add(88)
scores.add(62)
for score in scores
var grade = grade_to_letter(score)
print("${score} = ${grade}")
Exercise 2: Find Maximum
Iterate through a list and find the maximum value:
Solution
def find_max(nums: List(int)): int
if nums.count() == 0
return 0
var max_val = nums.at(0)
for num in nums
if num > max_val
max_val = num
return max_val
def main()
var nums = List(int)()
nums.add(10)
nums.add(45)
nums.add(23)
nums.add(89)
nums.add(34)
var max = find_max(nums)
print("Max: ${max}") # 89
Exercise 3: Filter With Control Flow
Count elements matching a condition:
Solution
def count_evens(nums: List(int)): int
var count = 0
for num in nums
if num % 2 == 0
count = count + 1
return count
def main()
var nums = List(int)()
nums.add(1)
nums.add(2)
nums.add(3)
nums.add(4)
nums.add(5)
nums.add(6)
var even_count = count_evens(nums)
print("Even numbers: ${even_count}") # 3
Next Steps
- → 06-Strings-and-Unicode — Text manipulation
- → 05-Control-Flow — Combine control flow with strings
- 🏋 Project-1-CLI-Tool — Use loops and conditions extensively
Key Takeaways
- if/else handles simple binary choices
- Inline if (
if cond: stmt) compresses a simple branch to one line - If-expression (
var x = if cond: a else: b) produces a value —else:required; chainedelse ifnot supported in expression position - match (branch) is better for many cases
- for loops iterate over collections, ranges, and anything with
cue iter/cue next - Generators (
def f(): Iter(T)withyield) produce a sequence lazily, one value per request - while loops repeat based on conditions
- break exits loops, continue skips iterations
- Early return makes functions clearer
- Guard clauses prevent "arrow code" (deeply nested if statements)
Next: Head to 06-Strings-and-Unicode to work with text.