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
match
construct, with matches specified like<pattern> => <expression|statement>
, optionally with guards specified withif
expressions. In Swift, patterns are matched using theswitch
construct, with matches specified likecase <pattern>: <expression|statement>
, optionally with guards specified withwhere
expressions. (where
is 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
Option
type: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-likedefault
is simply a wildcard match pattern (_ => <-expression|statement>
).One significant difference: like its
if
blocks, Rust’smatch
blocks 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
break
statements, but in Rust they’re only used in loop constructs, while Swift (like C) uses them to escapecase
s 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
switch
statements 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 thefallthrough
keyword. 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
guard
s, which allow you to leave normal or expected control flow in the main body of a block, with a secondary block for cases where theguard
isn’t matched. This isn’t a huge deal, but it does fit as a nice convenience into the typicalif let
pattern in Swift. Basically, it just lets you elide an emptyif
block and supply only theelse
block.Edit: a friend points out that Swift
guard
s also require you to exit the current scope, so it’s unambiguous what you’re doing if you use them.