Lesson 7 of 25 · Control flow

Loops

A loop does something several times: a fixed number of times, over a range, while a condition holds, or once for every item in a list. Salam has three loop words, repeat, until and each, and each one fits a different job.

repeat

repeat 3: runs the body three times. Add in i to get a counter called i that starts at zero, so repeat 4 in i gives 0, 1, 2 and 3. The count may be any whole number expression, such as a variable or v.len(), and a count of zero runs the body no times at all. repeat 1 to 10 walks a range with both ends included, by 3 sets the step, and if the start is greater than the end the loop counts down.

loop-count.salam
func main:
    repeat 3:
        println "hello"
    end
    repeat 4 in i:
        print i, ""
    end
    println ""
    repeat 10 to 0 by 5 in i:
        println "counting down", i
    end
end
Outputhello hello hello 0 1 2 3 counting down 10 counting down 5 counting down 0

The counter belongs to the loop: assigning to i inside the body is a compile error, so the loop always runs the number of times its header says.

Ranges and steps

The step after by is always written as a positive number, even when the loop counts down: the bounds alone decide the direction, so repeat 10 to 1 by 3 gives 10, 7, 4 and 1. The step may be a variable. A step of 0, or a negative number written in the code such as by -2, is a compile error (E063). A step held in a variable is only known at run time and is not checked: if it turns out negative, the loop quietly runs zero times.

loops.salam
func main:
    repeat 1 to 10 by 3 in i:
        println "round", i
    end
    repeat 10 to 1 by 3 in i:
        print i, ""
    end
    println ""
    step := 15
    repeat 0 to 60 by step in minute:
        print minute, ""
    end
    println ""
end
Outputround 1 round 4 round 7 round 10 10 7 4 1 0 15 30 45 60

The direction is decided when the loop starts, from the values the bounds have at that moment. That leads to a trap when a bound is a variable. repeat from to 1 counts down from 3 as expected, but when from is 0 the start is below the end, so the loop counts up and prints 0 1 instead of nothing:

loop-range-trap.salam
func countdown(from: int):
    repeat from to 1 in i:
        print i, ""
    end
    println "go"
end

func safe_countdown(from: int):
    if from >= 1:
        repeat from to 1 in i:
            print i, ""
        end
    end
    println "go"
end

func main:
    countdown(3)
    countdown(0)
    safe_countdown(0)
end
Output3 2 1 go 0 1 go go

If the start can fall below the end, guard the loop with an if, as safe_countdown does.

The until loop

until condition: repeats for as long as the condition holds, and stops once it becomes false. It is the loop other languages call while; Salam has no while keyword. So write the condition the way you want the loop to carry on: until n != 0 keeps going while n is not zero. Reach for until when you do not know in advance how many turns are needed.

loop-until.salam
func digit_sum(number: int): int:
    mut n := number
    mut sum := 0
    until n != 0:
        sum += n % 10
        n = n / 10
    end
    ret sum
end

func main:
    println digit_sum(2026), digit_sum(0)
    mut savings := 1000.0
    mut years := 0
    until savings < 2000.0:
        savings = savings * 1.08
        years += 1
    end
    println "doubled after", years, "years"
end
Output10 0 doubled after 10 years

The condition is checked before every turn, including the first. That is why digit_sum(0) gives 0: the body never runs. It is also why an inverted condition is dangerous. Write until n == 0 by mistake and the loop runs zero times, with no error at all. When a loop seems to do nothing, check which way round its condition is before anything else.

Loops that end with break

Sometimes the place to decide whether to stop is the middle or the end of the body, not the top. Write until true: and leave with break where the decision falls. This is also how to write a loop that runs at least once, which other languages call do-while. The example pages through 23 search results, 10 to a page, and stops after the page that holds the last result.

loop-forever.salam
func main:
    total_items := 23
    page_size := 10
    mut page := 1
    until true:
        first := (page - 1) * page_size + 1
        mut last := page * page_size
        if last > total_items:
            last = total_items
        end
        println "page", page, "shows items", first, "to", last
        if last == total_items:
            break
        end
        page += 1
    end
end
Outputpage 1 shows items 1 to 10 page 2 shows items 11 to 20 page 3 shows items 21 to 23

until false:, on the other hand, is a compile error, because its body could never run.

each ... in

each item in list walks every element of an array, a vector or a map. The form each index, item in list gives you the position as well.

loop-each.salam
func main:
    cities := ["Tehran", "Isfahan", "Shiraz"]
    each city in cities:
        println city
    end
    each number, city in cities:
        println number + 1, city
    end
end
OutputTehran Isfahan Shiraz 1 Tehran 2 Isfahan 3 Shiraz

A map always needs two names, each key, value in map, and a name you do not need is written _, because an unused loop variable is an error. The loop variable is a copy of the element and cannot be assigned. To change the elements themselves, take the index and write through it, as the price update below does.

loop-each-map.salam
func main:
    mut stock := HashMap {} as HashMap<str, int>
    defer stock.free()
    stock.put("pen", 12)
    stock.put("ink", 0)
    stock.put("paper", 250)
    stock.put("clip", 3)
    mut units := 0
    mut low := 0
    each _, count in stock:
        units += count
        if count < 5:
            low += 1
        end
    end
    println "units in stock:", units
    println "items to reorder:", low
    mut prices := [100, 250, 40]
    each i, price in prices:
        prices[i] = price + price / 10
    end
    println prices
end
Outputunits in stock: 265 items to reorder: 2 [110, 275, 44]

A map is walked in its own internal order, not the order the keys were added, so do not rely on that order; this example only adds things up. A string cannot be walked with each directly. To visit its characters, walk str.Chars(s), which is covered in the strings lesson.

Nested loops

A loop inside another loop runs completely for every turn of the outer one. Here the outer loop picks a row, the inner loop fills its columns, and fmt.PadLeft keeps the numbers lined up.

loop-nested.salam
import fmt

func main:
    repeat 1 to 4 in row:
        mut line := ""
        repeat 1 to 6 in col:
            line += fmt.PadLeft("" + row * col, 4)
        end
        println line
    end
end
Output 1 2 3 4 5 6 2 4 6 8 10 12 3 6 9 12 15 18 4 8 12 16 20 24

break and continue

break leaves the loop and continue jumps to the next turn. Inside nested loops, break 2 leaves two loops at once, break 3 three, and so on; the number cannot be larger than the number of loops around it.

loop-break.salam
func main:
    repeat 1 to 10 in i:
        if i % 2 == 0:
            continue
        end
        if i > 7:
            break
        end
        print i, ""
    end
    println ""
    repeat 3 in a:
        repeat 3 in b:
            if a + b == 3:
                break 2
            end
            println a, b
        end
    end
end
Output1 3 5 7 0 0 0 1 0 2 1 0 1 1

In an until loop, be careful with continue: it jumps straight back to the condition, so if the line that moves the loop forward comes after the continue, it is skipped and the loop never ends. Update the counter before you continue.

Which loop to use

You want toWrite
Do something a known number of timesrepeat n: or repeat n in i:
Walk a range of numbersrepeat a to b by step in i:
Visit every element of a collectioneach x in xs: or each i, x in xs:
Keep going while something is trueuntil condition:
Decide in the middle of the body, or run at least onceuntil true: with break

Rules worth remembering

  • until loops while its condition is true. An inverted condition makes the loop run zero times without any error.
  • The direction of repeat a to b comes from the bounds when the loop starts, so a variable start below the end counts up.
  • The step after by is positive. Zero or a negative literal is a compile error; a negative step held in a variable makes the loop silently run zero times.
  • Loop variables cannot be assigned. Change a collection through its index.
  • A loop that could never run, such as until false:, is a compile error.

Loops over arrays, vectors and maps come back in the lessons on arrays and collections. To pick one of several cases inside a loop, see match and switch.