Yupâ@jrose and a couple other people were discussing property-based destructuring on Twitter last weekend, and I ended up getting nerd-sniped.
The implementation there is very quick-and-dirty; it's actually done by implicitly forming a tuple containing the properties and then matching that instead of the original value, since that let me reuse more of the existing implementation. These patterns also don't work correctly in a bunch of places, like in if case
(where they still force a tuple context on the right-hand side of the match). But I found it useful to play around with a little.
To think about the syntax we want, we should start from the principles of the existing pattern-matching syntax. Patterns tend to echo the syntax used to form a value that would match the pattern:
- You match an enum case by writing
.caseName
because you would create an instance of that case by writing .caseName
.
- You match a tuple by writing
(name: subpattern, name: subpattern)
because you would create an instance of that tuple by writing (name: value, name: value)
.
- You match a value's type by writing
subpattern as SomeType
because you would cast an instance to that type by writing value as SomeType
.
That means square brackets probably aren't a good fit for this feature. They're pretty strongly associated with collection literals; if I saw square brackets in a pattern match, I would expect them to destructure collections by element:
[let first, let rest...] = myArray
["x": let x, "y": let y] = myDictionary
Curly brackets {...}
also aren't used in the syntax for initializing a property-holding instance. And they have a pretty serious ambiguity problem in at least one position where you should be able to write a pattern:
print("hello")
{foo: let foo, bar: let bar} = baz
// The compiler will think that pattern is a trailing closure.
(This ambiguity is why, unlike C, Swift doesn't allow you to write a bare block as a statementâyou have to put do
in front of it.)
So what is the right syntax? Well, if the goal is to write something similar to the code that would have created the value, the most obvious answer is to use something that looks like an initializer call:
Baz(foo: let foo, bar: let bar) = baz
case Baz(foo: let foo, bar: 14):
But arbitrary expressions are actually allowed in the patterns of case
statements, so this could change the interpretation of existing code. It's also a bit strange because in an expression, this syntax needs to match a specific initializer, but in a pattern, it would just be naming arbitrary properties in an arbitrary order.
So I think it's better to extend the existing tuple syntax to also cover other types with properties. The initialization analogy is not as direct without the type name, but it's still true that you put labeled values in parentheses to create the value, so they still at least rhyme.
The simplest way we could do this (and the one I built a quick-and-dirty prototype of) is to simply use the exact same syntax as a tuple, but reinterpret it when the type checker finds that the value being destructured isn't a tuple:
(foo: let foo, bar: let bar) = baz
case (foo: let foo, bar: 14):
This reads really nicely and I think it's pretty easy to understand what's happening, but it does have some drawbacks. The two features have different rulesâwhen you destructure a tuple, you have to name all fields in the correct order ("tuple shuffles" do actually work but they're deprecated); when you destructure something else, you can name properties in any order and you don't have to name all of them. This gives me pause for two reasons:
-
Tuple destructuring and propertywise destructuring share a syntax, but tuple destructuring has an important correctness featureâyou have to name every field and describe what to do with itâthat propertywise destructuring does not have. This means that they share a syntax, but one of them is prone to bugs that the other prevents.
-
You might actually want the propertywise behavior with a tuple sometimes. Tuple shuffles shouldn't be implicit because they're too easy to do accidentally, but explicit tuple shuffles would actually be pretty handy.
I'm not sure that these disqualify this syntax, but they do make me want to think about alternatives. One way to go might be to add an explicit indication that there are other, omitted properties. I favor using _...
instead of just _
, since _
already means "any single instance":
(foo: let foo, bar: let bar, _...) = baz
case (foo: let foo, bar: 14, _...):
Another option might be to use key path syntax instead of bare names; this drifts away a bit from the idea of using the same syntax that formed the instance, but it's very clear:
(\.foo: let foo, \.bar: let bar) = baz
case (\.foo: let foo, \.bar: 14):
(It also suggests that perhaps we could support other features allowed by key pathsâlike multiple components, subscripts, optional chaining, and perhaps in the future method calls.)
For that matter, the backslash is arguably unnecessary, and we could just use a leading dot:
(.foo: let foo, .bar: let bar) = baz
case (.foo: let foo, .bar: 14):
I think any of these options (tuple syntax, tuple with repeated wildcard, keypath-labeled, and leading-dot-labeled) would make immediate sense when read. My best guess is that tuple syntax and keypath-labeled syntax would be the easiest to remember how to write, since they're so closely analogous to existing features, but that's very much a guess.
Any thoughts?
P.S. One thing: Whatever solution we adopt, I think it's important that the properties be explicitly named, and that these names be separate from the names of the variables you're declaring. This is necessary to allow the feature to compose well with other pattern-matching features. You can sometimes get away without this in tuples because tuple fields come in a specific order, but that isn't true for structs, classes, and other property-containing types.