Hi everyone. SE-0390 left pattern matching as a consuming
operation. For noncopyable enum
s, that makes it impossible to access their payloads without also destroying the value, which is obviously insufficient. Even for copyable types, we’ve also long lacked the ability to modify an enum’s payload in place. It’s time to start talking about how to generalize pattern matching to support borrowing and inout
pattern matches, allowing for values to be matched without copying or consuming them, and also allowing for in-place mutation of enums during pattern matching. Before getting too deep into the design and implementation, I wanted to get some feedback about a few design points:
- the introduction of new binding patterns,
borrowing x
andinout x
, for borrowing and mutating the matched part of the value respectively - establishing the ownership behavior of various patterns
- how to syntactically distinguish borrowing, consuming, and mutating pattern matches (if at all)
- how to extend related conditionals, such as
if let
andif case
Thank you for reading, and for offering your feedback!
borrowing
and inout
pattern bindings
We want to introduce borrowing
and inout
bindings as a general language feature, and they are also an essential feature for noncopyable switch
patterns. We want a binding syntax for patterns that can be consistently applied to freestanding local binding declarations. As a starting point, I’ll use borrowing x
and inout x
as the syntax for borrowing and inout
pattern bindings respectively, since those keywords are consistent with what we currently use for parameter ownership modifiers.
Ownership behavior of patterns
Let’s look over all the different kinds of pattern Swift currently supports and work through the ownership behavior they require:
- Binding patterns in their current forms,
let a
orvar a
, take part of the matched value and bind a new variable to it. The new variable has independent ownership, so in the general case, it has to consume the matched value. The new binding forms,borrowing a
andinout a
, would be able access the part of the valueby borrowing or mutating the part of the value they match, respectively. - Wildcard patterns
_
discard the matched part of the value. This is ownership agnostic and can be considered to borrow, “mutate”, or consume the matched value if necessary. - Tuple patterns
(_, _)
break a tuple down into its elements, and then match each element against the corresponding subpattern. The tuple destructuring itself is ownership agnostic, since we can consume a tuple to allow the elements to be consumed, borrow a tuple to provide borrows of the elements, or exclusively access the tuple to allow exclusive access to each of the elements. As such, the ownership behavior of the tuple pattern itself can come from the needed behavior of its subpatterns. - Enum patterns
.case(_, _)
match when an enum contains a value of the specified case, and then match the element(s) of the associated value, if any, to the corresponding subpattern(s). This is also ownership agnostic, and the ownership behavior can arise from that needed by the subpatterns. - Optional unwrapping patterns
_?
are essentially sugar for the enum patternOptional.some(_)
, and so are also ownership agnostic. - Boolean patterns
true
andfalse
can test the boolean value while borrowing it. - Dynamic cast patterns
is T
or_ as T
dynamically cast the matched value toT
, and if the cast succeeds, tests the cast result against the subpattern (or succeeds immediately in the case ofis T
). Dynamic casting isn’t currently supported for noncopyable types, but if it were, many forms of cast would need to transfer ownership from the cast operand to the result (for instance, to wrap it in an existential in the case of anas P
cast), so in the general case a dynamic cast would have to consume the value being matched. We may be able to relax this in the future for certain kinds of cast where the result can always be borrowed out of part of the original. - Expression patterns take the value of an arbitrary expression and match it against the value being matched using the
~=
operator. The ownership behavior of an expression pattern has to depend on the ownership of the parameter to the~=
overload chosen for the match. Most of the standard library’s~=
implementations only need borrowing access in practice, and this is likely to be the common case.
The aggregate patterns (tuple and enum) are ownership agnostic themselves, but can contain zero, one, or many subpatterns, so we also have to consider the composed ownership behavior of compound patterns. Luckily, there is a strict ordering of capabilities among the three ownership behaviors: any valid value the code has access to can be borrowed (assuming there are no exclusive accesses in action for the duration of the borrow). On the other hand, a value can only be exclusively accessed if the value’s exclusivity can be proven, but code that does have exclusive access can provide shared borrows to the value too, temporarily giving up exclusivity. And finally, a value can only be consumed from a context with full ownership of the value, though with full ownership, code can give out either exclusive or shared borrows. Therefore, we can say that the ownership behavior of an aggregate pattern is the strictest ownership behavior of its components: if all of the component patterns can borrow, then the pattern as a whole borrows. If any component pattern requires exclusive access, but no components need to consume, then the aggregate pattern is mutating. And finally, if any component pattern consumes, then the aggregate pattern consumes.
Some examples:
case _: // borrowing
case let a: // consuming
case borrow a: // borrowing
case inout a: // mutating
case (borrow a, borrow a): // borrowing
case (inout a, borrow b): // mutating
case (borrow a, let b): // consuming
case (inout a, let b): // consuming
Determining the ownership behavior of a pattern match
To determine the overall effect of a switch
on its subject, we can choose to:
- require a syntactic marker on the switch subject itself, or
- infer the necessary ownership from the patterns applied
or some combination of the two. From surveying the pattern forms above, it seems to me that the ownership requirements for a switch
should be determinable from the patterns in the switch
during type checking, so a syntactic signifier isn’t strictly necessary. Nonetheless, for mutating pattern matches, we may at least want to require the &
marker like we do for inout
arguments in function calls:
switch &x {
case .foo(inout foo):
modify(&foo)
}
SE-0390 imposed the requirement that switching over a noncopyable local variable be written with the consume
operator, switch consume x { … }
, as a way of future-proofing in case we did need to drive a syntactic wedge between borrowing and consuming switches, but we could choose to relax this requirement. We currently don’t require any syntactic distinction between borrowing and consuming parameters in function calls, so it would b434 consistent to say that there is no syntactic distinction necessary between borrowing and consuming pattern matches.
Ownership control in if let
and if case
We also allow forms of pattern matching in if
, while
, and guard
conditionals, using the let
/var
and case
pattern forms (often called if let
and if case
colloquially, even though they can also be used with while
and guard
). if let
and if var
can be looked at as a shorthand for pattern-matching an Optional
, as if by if case .some([let|var] x)
, so if we introduce borrowing
and inout
pattern bindings, then it’s reasonable to expect these new binding forms to be usable for Optional
unwrapping, as if borrowing x = optional
and if inout x = &optional
.
Meanwhile, if case
is like a simplified switch
against a single pattern, so the ownership behavior of an if case
can be determined from that one pattern’s ownership requirements. The right-hand side of the =
would behave like a switch
subject, needing a &
when the pattern is mutating but otherwise accepting a bare value:
if case .foo(let x) = value { ... } // consuming match
if case .foo(borrowing x) = value { ... } // borrowing match
if case .foo(inout x) = &value { ... } // mutating match