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.
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
"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)
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
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.
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.