Rust and Swift (vii)
Pattern matching and the value of expression blocks.
I am reading through the Swift book, and comparing it to Rust, which I have also been learning over the past month. As with the other posts in this series, these are off-the-cuff impressions, which may be inaccurate in various ways. I’d be happy to hear feedback! Note, too, that my preferences are just that: preferences. Your tastes may differ from mine. (See all parts in the series.)
Both Rust and Swift have pattern-matching, and with what appears to be fairly similar basic behavior. (I touched on this briefly in my first post in the series.) In Rust this goes under the
matchconstruct, with matches specified like<pattern> => <expression|statement>, optionally with guards specified withifexpressions. In Swift, patterns are matched using theswitchconstruct, with matches specified likecase <pattern>: <expression|statement>, optionally with guards specified withwhereexpressions. (whereis also used in Rust, but for generic constraints, not pattern match guards.)Both languages allow you to bind names to a matched pattern: Swift with
case let <name>and Rust simply by using the name in a normal destructuring expression as part of the match definition.Edit: that’s not quite right. In Rust, you use the
@operator with the variable name you want to bind in the match.Edit the second: I was mixed up, because Rust actually has both of those options. You can either match directly, e.g. when getting the value of an
Optiontype:Some(value)as the pattern will bindvalue. But if you need to bind a specific part of more complicated data structure, the@operator is present to let you do it in a fairly straightforward way.Both languages allow for the use of
_as a “wildcard” in match definitions. Since match definitions in Rust use the patterns directly, the equivalent of Swift’s C-likedefaultis simply a wildcard match pattern (_ => <-expression|statement>).One significant difference: like its
ifblocks, Rust’smatchblocks are expressions, so they can be assigned. I.e., you can write this:let test = 5u32; let description = match test { 0..10 => "less than ten", _ => "greater than ten", } println!("{?:}"); // "less than ten"Swift doesn’t let you do this; the same thing there would be written like this:
let test: UInt32 = 5 var description: String switch test { case 0..<10: description = "less than ten" default: description = "greater than ten" } println("\(description)")Both languages have
breakstatements, but in Rust they’re only used in loop constructs, while Swift (like C) uses them to escapecases as well. The Swift book gives an example of one place they’re necessary in aswitch: to match a case and do nothing there (e.g.default: break). In Rust, you would simply supply an empty block for that scenario (e.g._ => {}).Correctly, both languages force you to match exhaustively on relevant patterns. If you’re matching an enumerated type, for example, you must handle every enumerated value. You can of course do this with wildcard patterns or with Swift’s
default, but the good thing is that both languages will refuse even to compile if a given pattern isn’t matched.Swift’s default behavior around its
switchstatements is sane: it does not automatically fall through into the next statement. It does let you do this, without checking the condition on the next statement (as in C), using thefallthroughkeyword. Rust, by contrast, simply doesn’t allow this at all.Both languages supply named control statements (loops, etc.), with slightly different syntax for naming them. Rust’s, curiously, shares its syntax with lifetime definitions—more on those in a future post.
I don’t believe Rust has anything quite like Swift’s
guards, which allow you to leave normal or expected control flow in the main body of a block, with a secondary block for cases where theguardisn’t matched. This isn’t a huge deal, but it does fit as a nice convenience into the typicalif letpattern in Swift. Basically, it just lets you elide an emptyifblock and supply only theelseblock.Edit: a friend points out that Swift
guards also require you to exit the current scope, so it’s unambiguous what you’re doing if you use them.
Chris Krycho