Lesson 8 of 25 · Control flow

match and switch

When a value has several possible cases, reach for match or switch instead of a chain of ifs. match compares a value against patterns and checks that you have covered every case; switch is a statement in the style of C, with ranges, comparisons and fallthrough.

match

match is an expression: it compares a value against the cases and gives back the result of the branch that matched. Each branch is a pattern, a colon, the result and end. Several cases can share one branch by separating them with commas, and else catches everything left over.

match-values.salam
func grade(score: int): str:
    ret match score / 10:
        10, 9: "excellent" end
        8: "very good" end
        7: "good" end
        else: "needs more work" end
    end
end

func main:
    println grade(95), grade(80), grade(45)
end
Outputexcellent very good needs more work

When match gives back a value, every branch has to produce one, all of the same type.

match as a statement

match can also stand on its own as a statement. A branch may then hold several lines, closed by its end. The subject does not have to be a number: strings work too, and are compared by their contents. The example plays back a list of commands, the way a small command-line tool or chat bot would.

match-command.salam
func main:
    commands := ["start", "status", "stop", "jump", "quit"]
    mut running := false
    each cmd in commands:
        match cmd:
            "start", "run":
                running = true
                println "starting"
            end
            "stop":
                running = false
                println "stopping"
            end
            "status":
                println running ? "running" : "stopped"
            end
            "quit": println "bye" end
            else: println "unknown command:", cmd end
        end
    end
end
Outputstarting running stopping unknown command: jump bye

Unlike a switch, a match branch never falls into the next one; exactly one branch runs. The patterns for a number or a string are exact values. Ranges such as 1 to 9 and comparisons such as < 0 are not match patterns: for those use switch, shown below.

Matching an enum

Over an enum, the patterns are the names of its cases. The bare name Spring is enough, and the full Season.Spring works too. match checks that no case has been left out:

match.salam
enum Season:
    Spring, Summer, Autumn, Winter
end

func weather(s: Season): str:
    ret match s:
        Spring, Autumn: "mild" end
        Summer: "hot" end
        Winter: "cold" end
    end
end

func main:
    println "spring weather:", weather(Season.Spring)
    println "winter weather:", weather(Season.Winter)
end
Outputspring weather: mild winter weather: cold

Delete the Winter branch and the program no longer compiles, and the error names the missing case. So when you add a case to an enum later, the compiler shows you every match that has to learn about it. An else branch turns that check off, so leave it out when you can list every case.

Pulling out data, and guards

When enum cases carry data, a pattern can take that data apart: Cash(a) binds the amount to a, and _ skips a value you do not need. Card c binds the whole case, so its fields are reached as c.amount. A branch can also add a guard: if and a condition after the pattern. The guard can use the names the pattern bound, and if it is false, matching goes on to the next branch.

match-data.salam
enum Payment:
    Cash(amount: int)
    Card(amount: int, last4: str)
    Voucher(code: str)
    Free
end

func receipt(p: Payment): str:
    ret match p:
        Cash(a) if a >= 1000: "cash " + a + ", count it twice" end
        Cash(a): "cash " + a end
        Card(a, last4): "card ending " + last4 + ", " + a end
        Voucher(_): "paid with a voucher" end
        Free: "nothing to pay" end
    end
end

func main:
    println receipt(Payment.Cash(1500))
    println receipt(Payment.Cash(20))
    println receipt(Payment.Card(80, "4421"))
    println receipt(Payment.Voucher("SPRING"))
    println receipt(Payment.Free)
    p := Payment.Card(35, "0042")
    label := match p:
        Card c: "card payment of " + c.amount end
        else: "another kind of payment" end
    end
    println label
end
Outputcash 1500, count it twice cash 20 card ending 4421, 80 paid with a voucher nothing to pay card payment of 35

A guarded branch does not count as covering its case, which is why Cash(a) appears a second time without a guard. Enums with data are introduced in the enums lesson.

Struct patterns

match can test a struct against its fields. x = 0 means the field has to equal zero, and a bare name such as y binds the field's value to a variable of that name. Because patterns carry conditions, a final else is required.

match-struct.salam
struct Point:
    pub x: int = 0
    pub y: int = 0
end

func where(p: Point): str:
    ret match p:
        Point { x = 0, y = 0 }: "the origin" end
        Point { x = 0, y }: "on the vertical axis, y = " + y end
        Point { x, y = 0 }: "on the horizontal axis, x = " + x end
        else: "somewhere else" end
    end
end

func main:
    println where(Point {})
    println where(Point { y = 5 })
    println where(Point { x = 3 })
    println where(Point { x = 1, y = 2 })
end
Outputthe origin on the vertical axis, y = 5 on the horizontal axis, x = 3 somewhere else

switch

switch is a statement for choosing between cases. A case label can be one value, several values separated by commas, or a range such as 1 to 3. If a case does not end with break, execution falls into the next one, so write break wherever you do not want that.

switch.salam
func main:
    repeat 5 in day:
        switch day:
            0:
                println day, "Monday, the week begins"
                break
            end
            1 to 3:
                println day, "the middle of the week"
                break
            end
            else:
                println day, "the weekend is close"
            end
        end
    end
end
Output0 Monday, the week begins 1 the middle of the week 2 the middle of the week 3 the middle of the week 4 the weekend is close

A switch label may begin with a comparison operator, such as >= 90. switch true takes the place of a chain of ifs: every label is a condition, and the first one that holds runs.

switch-true.salam
func main:
    repeat 3 in i:
        temperature := i * 15
        switch true:
            temperature < 10:
                println temperature, "cold"
                break
            end
            temperature < 25:
                println temperature, "mild"
                break
            end
            else:
                println temperature, "hot"
            end
        end
    end
    score := 75
    switch score:
        >= 90:
            println "excellent"
            break
        end
        >= 60:
            println "pass"
            break
        end
        else:
            println "fail"
        end
    end
end
Output0 cold 15 mild 30 hot pass

Falling through on purpose

Fallthrough is usually something to guard against, but it is handy when each case adds to the ones below it. In this subscription plan, Premium has everything Standard has and more, and Standard has everything Basic has. Ordering the cases from the richest down and leaving out break lists each plan's features with no repetition.

switch-fallthrough.salam
enum Plan:
    Basic, Standard, Premium
end

func features(p: Plan):
    println "plan", p.name(), "includes:"
    switch p:
        Premium:
            println "  offline downloads"
        end
        Standard:
            println "  HD video"
        end
        Basic:
            println "  the full catalogue"
        end
    end
end

func main:
    features(Plan.Premium)
    features(Plan.Basic)
end
Outputplan Premium includes: offline downloads HD video the full catalogue plan Basic includes: the full catalogue

A switch on an enum must cover every case or have an else; otherwise it is error E103, just as with match. A switch on anything else that finds no matching label simply does nothing.

switch inside a loop

Inside a switch, break leaves the switch only, not the loop around it. To leave the loop as well, write break 2. continue is not caught by the switch: it goes straight to the next turn of the loop. The example averages sensor readings, skips the value that means "offline" and stops at the end marker.

switch-loop.salam
func main:
    readings := [21, -1, 23, 999, 25, 0, 30]
    mut total := 0
    mut count := 0
    each r in readings:
        switch r:
            -1:
                println "sensor offline, skipped"
                continue
            end
            999:
                println "end-of-data marker, stopping"
                break 2
            end
            else:
                total += r
                count += 1
            end
        end
        println "reading", r, "accepted"
    end
    println "average of", count, "readings:", total / count
end
Outputreading 21 accepted sensor offline, skipped reading 23 accepted end-of-data marker, stopping average of 2 readings: 22

match or switch?

SituationUse
You need a value backmatch
Enum cases carry data, or you need guards or struct patternsmatch
Ranges such as 10 to 19, or labels such as >= 90switch
One case should continue into the nextswitch without break
Several statements per case, on a plain enum or valueeither; both check that an enum is fully covered
A handful of unrelated conditionsswitch true, or plain if and else

Things to remember

  • A match branch never falls through; a switch case always does, unless it ends with break.
  • Enum patterns are case names, bare (Spring) or with the enum in front (Season.Spring).
  • A match on an enum must cover every case, and so must a switch on an enum (E103), unless there is an else.
  • A guarded branch, or a struct pattern with a test, does not cover its case on its own.
  • break in a switch leaves the switch; break 2 also leaves the loop around it.
  • In a switch, else must be the last label.