Pattern Matching

Control flow—deciding which code to execute—is a big part of imperative languages. Magpie has your basic looping and branching expressions that you know from most imperative languages. But instead of a boring C-style switch, it's got something turbo-charged: pattern matching.

A match expression evaluates a value expression. Then it looks at each of a series of cases. For each case, it tries to match the case's pattern against the value. If the pattern matches, then it evaluates the body of the case.

val fruit = "lemon"
match fruit
    case "apple" then print("apple pie")
    case "lemon" then print("lemon tart")
    case "peach" then print("peach cobbler")
end

The expression after match (here just fruit) is the value being matched. Each line starting with case until the final end is reached defines a potential branch the control flow can take. When a match expression is evaluated, it tries each case from top to bottom. The first case here doesn't match because "apple" isn't "lemon", so its body is skipped. The second case does match. That means we execute its body, print "lemon tart" and we're done. Once a case has matched, the remaining cases are skipped.

Like everything in Magpie, match expressions are expressions, not statements, so they return a value: the result of evaluating the body of the matched case. That means we can reformulate the above example like so:

val fruit = "lemon"
val dessert = match fruit
    case "apple" then "apple pie"
    case "lemon" then "lemon tart"
    case "peach" then "peach cobbler"
end
print(dessert)

Or even:

val fruit = "lemon"
print(match fruit
    case "apple" then "apple pie"
    case "lemon" then "lemon tart"
    case "peach" then "peach cobbler"
end)

A case body may also be a block, as you'd expect. If it's the last case in the match, the block must end with end, otherwise, the following case is enough to terminate it:

match dessert
    case "apple pie" then
        print("apple")
        print("pie crust")
        print("ice cream")
    case "lemon tart" then // "case" here ends "apple pie" block
        print("lemon")
        print("pastry shell")
    end // last case block must end with "end"
end // ends entire "match" expression

Case Patterns

With simple literal patterns, this doesn't look like much more than switch statements in other languages, but Magpie allows you to use any pattern as a case. With that, you can bind variables, destructure objects, or branch based on type:

def describe(obj)
    match obj
        case b is Bool          then "Bool : " + b
        case n is Int           then "Int : " + n
        case s is String        then "String : " + s
        case x is Int, y is Int then "Point " + x + ", " + y
    end
end

describe(true) // "Bool : true"
describe(123)  // "Int : 123"
describe(3, 4) // "Point : 3, 4"

If the pattern for a case binds a variable (like b in the first case here) that variable's scope is limited to the body of that case. That way, you can ensure that you'll only get a variable bound if it matches what you want. For example, here we know for certain that b will only exist if obj is a boolean and b will be its value.

Match Failure

It's possible for no case in a match expression to match the value. If that happens, it will throw a NoMatchError. This is the right thing to do if you only expect certain values and a failure to match is a programmatic error. If you do want to handle any possible value, though, you can add an else case to the match expression:

val dessert = match fruit
    case "apple" then "apple pie"
    case "lemon" then "lemon tart"
    case "peach" then "peach cobbler"
    else "unknown fruit"
end

If no other pattern matches, the else case will.