Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 true
  • else if condition — Try the next condition (Zebra spells the chain else 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 true
  • or — At least one must be true
  • not — 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: b is 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's i lives 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, and nil once the body has finished.

The rules:

  • yield v produces a value. A bare return finishes early; a generator cannot return a value ("a generator finishes with a bare return; it produces values with yield").
  • 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 cannot yield (yet).
  • A yield inside branch, try, with, using or allocate is refused by name; restructure with an if chain. A for that yields can walk a List, a range, or another iterable (a generator included), with one loop variable and no if filter 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; chained else if not 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) with yield) 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.