Skip to content

Chapter 3: Control Flow

Making Decisions

Every interesting program makes decisions. Should we process this file? Is the user authenticated? Did the network request succeed?

The if statement is how you express these decisions in code:

wyn
fn main() -> int {
    var temperature = 25
    
    if (temperature > 30) {
        print("It's hot")
    }
    
    return 0
}

If the condition is true, the code in the braces runs. If it's false, it doesn't. Simple.

Notice the parentheses around the condition-they're required in Wyn. This is deliberate. It makes the code unambiguous and easier to parse, both for the compiler and for humans.

The else Branch

Often you want to do one thing if a condition is true, and something else if it's false:

wyn
fn main() -> int {
    var age = 18
    
    if (age >= 18) {
        print("Can vote")
    } else {
        print("Too young")
    }
    
    return 0
}

Chaining Conditions

When you have multiple conditions to check, chain them with else if:

wyn
fn main() -> int {
    var score = 85
    
    if (score >= 90) {
        print("A")
    } else if (score >= 80) {
        print("B")
    } else if (score >= 70) {
        print("C")
    } else if (score >= 60) {
        print("D")
    } else {
        print("F")
    }
    
    return 0
}

The conditions are checked in order. As soon as one is true, that branch runs and the rest are skipped. This matters:

wyn
var score = 95

if (score >= 60) {
    print("D")  // This runs first!
} else if (score >= 90) {
    print("A")  // Never reached
}

Order your conditions from most specific to least specific.

Loops: Doing Things Repeatedly

The while Loop

A while loop runs as long as its condition is true:

wyn
fn main() -> int {
    var count = 0
    
    while (count < 5) {
        print(count)
        count = count + 1
    }
    
    return 0
}

This prints 0, 1, 2, 3, 4. The condition is checked before each iteration. When count reaches 5, the condition becomes false and the loop stops.

Be careful with while loops. If the condition never becomes false, you have an infinite loop:

wyn
var count = 0
while (count < 5) {
    print(count)
    // Forgot to increment count!
}

This will run forever. Well, until you kill the process.

The for Loop

When you know how many times you want to loop, use for:

wyn
fn main() -> int {
    for (var i = 0; i < 5; i = i + 1) {
        print(i)
    }
    return 0
}

A for loop has three parts:

  1. Initialization: var i = 0 - runs once before the loop starts
  2. Condition: i < 5 - checked before each iteration
  3. Update: i = i + 1 - runs after each iteration

This is equivalent to:

wyn
var i = 0
while (i < 5) {
    print(i)
    i = i + 1
}

But the for loop is more compact and keeps the loop control in one place.

Iterating Over Arrays

The idiomatic way to loop through an array is for-in:

wyn
fn main() -> int {
    var numbers = [10, 20, 30, 40, 50]
    
    for n in numbers {
        println(n.to_string())
    }
    
    return 0
}

Need the index too? Use for i, v in:

wyn
fn main() -> int {
    var names = ["Alice", "Bob", "Charlie"]
    
    for i, name in names {
        println(i.to_string() + ": " + name)
    }
    // 0: Alice
    // 1: Bob
    // 2: Charlie
    
    return 0
}

For ranges, use for i in start..end:

wyn
for i in 0..5 {
    println(i.to_string())  // 0, 1, 2, 3, 4
}

You can also use the C-style for loop when you need more control:

wyn
fn main() -> int {
    var numbers = [10, 20, 30, 40, 50]
    
    for (var i = 0; i < numbers.len(); i = i + 1) {
        println(numbers[i].to_string())
    }
    
    return 0
}

Breaking Out: Control Statements

break: Exit Early

Sometimes you want to exit a loop before it naturally finishes:

wyn
fn main() -> int {
    var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
    for (var i = 0; i < numbers.len(); i = i + 1) {
        if (numbers[i] == 5) {
            print("Found 5!")
            break
        }
        print(numbers[i])
    }
    
    return 0
}

Output:

1
2
3
4
Found 5!

When break executes, the loop stops immediately. Control jumps to the first statement after the loop.

continue: Skip to Next Iteration

continue skips the rest of the current iteration and jumps to the next one:

wyn
fn main() -> int {
    for (var i = 0; i < 10; i = i + 1) {
        if (i % 2 == 0) {
            continue  // Skip even numbers
        }
        print(i)
    }
    return 0
}

Output:

1
3
5
7
9

Use continue when you want to skip certain cases without nesting more if statements.

return: Exit the Function

return exits the entire function, not just the loop:

wyn
fn find_first_negative(numbers: [int]) -> int {
    for (var i = 0; i < numbers.len(); i = i + 1) {
        if (numbers[i] < 0) {
            return numbers[i]  // Exit function immediately
        }
    }
    return 0  // No negative found
}

fn main() -> int {
    var nums = [5, 3, -2, 8, -1]
    var result = find_first_negative(nums)
    print(result)  // -2
    return 0
}

Patterns You'll Use Constantly

Searching

Finding something in a collection:

wyn
fn main() -> int {
    var names = ["Alice", "Bob", "Charlie", "Diana"]
    var target = "Charlie"
    var found = false
    
    for (var i = 0; i < names.len(); i = i + 1) {
        if (names[i] == target) {
            found = true
            break
        }
    }
    
    if (found) {
        print("Found!")
    } else {
        print("Not found")
    }
    
    return 0
}

This is the linear search algorithm. It's O(n)-you might check every element. For small arrays, that's fine. For large ones, you want a better data structure (we'll get to that).

Accumulation

Building up a result:

wyn
fn main() -> int {
    var numbers = [1, 2, 3, 4, 5]
    var sum = 0
    
    for (var i = 0; i < numbers.len(); i = i + 1) {
        sum = sum + numbers[i]
    }
    
    print(sum)  // 15
    return 0
}

Start with an initial value (often 0 or an empty collection), then update it in each iteration. This pattern is everywhere: summing, counting, building strings, constructing arrays.

Finding Maximum

wyn
fn main() -> int {
    var numbers = [3, 7, 2, 9, 1, 5]
    var max = numbers[0]  // Assume first is max
    
    for (var i = 1; i < numbers.len(); i = i + 1) {
        if (numbers[i] > max) {
            max = numbers[i]
        }
    }
    
    print(max)  // 9
    return 0
}

Note we start at index 1, not 0, since we already used the first element as our initial max.

Filtering

Selecting elements that match a condition:

wyn
fn main() -> int {
    var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    var evens = []
    
    for (var i = 0; i < numbers.len(); i = i + 1) {
        if (numbers[i] % 2 == 0) {
            evens.push(numbers[i])
        }
    }
    
    // evens is now [2, 4, 6, 8, 10]
    return 0
}

Nested Loops: When One Isn't Enough

Sometimes you need loops inside loops:

wyn
fn main() -> int {
    // Print a multiplication table
    for (var i = 1; i <= 5; i = i + 1) {
        for (var j = 1; j <= 5; j = j + 1) {
            print(i * j)
            print(" ")
        }
        print("\n")
    }
    return 0
}

The inner loop runs completely for each iteration of the outer loop. If the outer loop runs 5 times and the inner loop runs 5 times, you get 25 total iterations.

Be careful with nested loops-they multiply complexity. Two nested loops over arrays of size n give you O(n²) time complexity. That's fine for small n, but it gets slow fast.

Guard Clauses: Early Returns

Instead of nesting conditions, return early:

wyn
fn process_user(age: int, name: string) -> int {
    // Bad: nested conditions
    if (age >= 18) {
        if (name != "") {
            if (name.len() < 50) {
                // Actually do something
                print("Processing user")
            }
        }
    }
    return 0
}

This is hard to read. Use guard clauses instead:

wyn
fn process_user(age: int, name: string) -> int {
    // Good: guard clauses
    if (age < 18) {
        return 0
    }
    if (name == "") {
        return 0
    }
    if (name.len() >= 50) {
        return 0
    }
    
    // Actually do something
    print("Processing user")
    return 0
}

Check for invalid conditions first and return early. The main logic stays at the top level, not buried in nested blocks.

What You've Learned

Control flow is about making decisions and repeating actions. if statements let you branch based on conditions. Loops let you repeat code. break, continue, and return let you exit early.

The patterns-searching, accumulating, filtering-are fundamental. You'll use them in almost every program you write.

Guard clauses keep code flat and readable. Nested loops multiply complexity, so use them carefully.

These are the building blocks. Everything else is just combinations of these primitives.

Exercises

  1. FizzBuzz: Print numbers 1-100. For multiples of 3, print "Fizz". For multiples of 5, print "Buzz". For multiples of both, print "FizzBuzz". This is a classic interview question-make sure you can do it.

  2. Prime checker: Write a function that returns true if a number is prime, false otherwise. A number is prime if it's only divisible by 1 and itself.

  3. Reverse an array: Given an array, print its elements in reverse order. Don't create a new array-just iterate backwards.

  4. Find duplicates: Given an array, print any values that appear more than once. This is harder than it looks.

  5. Nested loops: Print a triangle pattern:

    *
    **
    ***
    ****
    *****
  6. Early exit: Write a function that searches for a value in an array and returns its index. If not found, return -1. Use return to exit as soon as you find it.

  7. Guard clauses: Take a function with nested if statements and rewrite it using guard clauses. Notice how much more readable it becomes.


Next: Chapter 4: Functions

Try It Yourself

Install Wyn and run this example:

bash
curl -sSL wynlang.com/install.sh | sh
wyn init hello && cd hello
wyn run src/main.wyn

MIT License - v1.19.1