Learn · Chapter 4 of 12

Control flow

How to make decisions and loop. The constructs are familiar; Capa lets most of them work both as statements and as expressions.

If, the statement form

fun main(stdio: Stdio)
    let n = 7
    if n > 0
        stdio.println("positive")
    else if n < 0
        stdio.println("negative")
    else
        stdio.println("zero")

No parentheses around the condition. Indentation marks each branch's body. The chain is if / else if / else; the final else is optional.

If, the expression form

The same construct also produces a value. When every branch produces a value of the same type, you can use if on the right-hand side of a let:

fun main(stdio: Stdio)
    let n = 7
    let sign = if n > 0 then "+" else if n < 0 then "-" else "0"
    stdio.println("sign is ${sign}")

The keyword then separates the condition from the value when if is used as an expression on a single line. The branches must all produce the same type, and the else branch is mandatory.

While

fun main(stdio: Stdio)
    var n = 3
    while n > 0
        stdio.println("${n}")
        n = n - 1
    stdio.println("liftoff")
$ capa --run countdown.capa
3
2
1
liftoff

The loop runs while the condition is true. break exits the loop; continue skips to the next iteration. Both must sit inside a loop or the compiler rejects them.

For, with ranges

The same countdown with for:

fun main(stdio: Stdio)
    for i in 0..3
        stdio.println("${i}")
$ capa --run hello.capa
0
1
2

The expression 0..3 is a range: it produces 0, 1, 2 in turn. The upper bound is exclusive. If you want it inclusive, use 0..=3:

for i in 0..=3
    stdio.println("${i}")        // prints 0 1 2 3

A range is its own type (Range<Int>). When you iterate directly with for x in a..b, no list is built; the iteration is lazy. Materialise to a list with .to_list() if you need one.

You can also iterate any List<T>:

fun main(stdio: Stdio)
    let names = ["Ana", "Bruno", "Carla"]
    for name in names
        stdio.println("Hello, ${name}")

Match, the gentle introduction

The full power of match arrives in chapter 6 (when we introduce sum types). For now you can already use it on primitive values:

fun describe(n: Int) -> String
    match n
        0 -> return "zero"
        1 -> return "one"
        _ -> return "many"

The shape is: scrutinee on the first line, then one arm per indented line. Each arm is pattern -> body. The _ wildcard matches anything that did not match a previous arm; it acts as a "default" branch.

Like if, match can also produce a value when every arm produces one and they share a type:

fun describe(n: Int) -> String
    return match n
        0 -> "zero"
        1 -> "one"
        _ -> "many"

The two error modes

First: mismatched branch types in expression form.

fun main(stdio: Stdio)
    let x = if true then 1 else "two"
    stdio.println("${x}")
$ capa --run hello.capa
hello.capa:2:13: error: if-expr: branches must have the same type;
                        got Int (then) vs String (else)
   2 |     let x = if true then 1 else "two"
                   ^

The then branch is Int, the else branch is String. There is no unifying type for both, so the analyzer rejects. If you want the result to be a String either way, write the 1 as "1".

Second: break outside a loop.

fun main(stdio: Stdio)
    break
$ capa --run hello.capa
hello.capa:2:5: error: 'break' is only valid inside a loop
   2 |     break
           ^

The same diagnostic shape fires for continue outside a loop.

Where you are now

You can branch, loop, and pattern-match on primitive values. The first four chapters give you a language that looks like Python with explicit types. From the next chapter onward, things get more interesting.

Chapter 5 is collections: lists, maps, sets, the higher-order methods that go with them. After that, you start building real programs.