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.