- Proposal: TBD
- Author: Brent Royal-Gordon
- Status: TBD
- Review manager: TBD
This proposal enhances optional binding with a new, tuple-based syntax for binding multiple values. It replaces functionality lost in SE-0099 with a syntax compatible with the new design.
Swift Evolution Discussion: [Accepted with Revision] SE-0099 Restructuring Condition Clauses
In Swift 2, it was possible to bind multiple optional values in a single
if let
, guard let
, or while let
clause:
guard let a = opt1, b = opt2, c = opt3 else { ... }
SE-0099
simplified the syntax of conditional statements, but removed this
feature so that ,
could instead
separate different conditional clauses. Code like this must now use
three separate optional binding clauses:
guard let a = opt1, let b = opt2, let c = opt3 else { ... }
The similar case
clause sidesteps this problem because it can
pattern-match tuples. Hence, you can put several patterns in a tuple on
the left side of the =
, and a matching number of values in a tuple on
the right side, and match them all with one case
clause. This doesn't
conflict with the clause separation syntax because the commas are
within parentheses.
In a pinch, this can be used to pattern-match against several optionals:
guard case let (a?, b?, c?) = (opt1, opt2, opt3) else { ... }
However, optional binding is itself redundant in exactly the same way. Optional binding exists because the pattern-matching syntax is too clumsy, and requires too much understanding from the user, for such a common operation. The same concerns apply when performing multiple optional bindings.
Swift's syntax actually permits you to write an optional binding clause analogous to the tuple pattern-match above:
guard let (a, b, c) = (opt1, opt2, opt3) else { ... }
However, Swift does not interpret this as an attempt to bind several optionals inside a tuple; instead it assumes you want to bind and destructure an optional tuple, and type-checking fails because the tuple is not optional.
error: initializer for conditional binding must have Optional type, not
'(Int?, Int?, Int?)' (aka '(Optional<Int>, Optional<Int>, Optional<Int>)')
We should extend optional binding clauses to permit a tuple of optional
values on the right of the =
and a tuple of constants with identical
arity on the left. Swift should test each element of the tuple on the
right, and if none of them are nil
, bind them to the constants on the
left.
Nothing in this proposal should change the way optional binding handles
an optional tuple (T, U)?
, as opposed to a tuple of optionals
(T?, U?)
. Even an optional tuple of optionals (T?, U?)?
should
continue to be handled as before.
No change to the formal grammar is necessary, as the pattern and initializer productions in the optional-binding-head rule can already match tuples:
optional-binding-head : 'let' pattern initializer
Rather, Sema should be modified to detect this situation and generate
appropriate code. Currently, TypeCheckPattern.cpp essentially converts
let a = opt1
into case let a? = opt1
; if this proposal is accepted,
it should similarly convert let (a, b) = (opt1, opt2)
into
case let (a?, b?) = (opt1, opt2)
.
Permitting deeper pattern matching of nested tuples is highly precedented by pattern matching, but is a niche feature. It should be supported if easily achievable.
guard let (a, (b, c)) = (opt1, (opt2, opt3)) else { ... }
Ideally, optional bindings whose initializer is an expression evaluating to a tuple of optionals would be supported:
let tuple = (opt1, opt2)
if let (a, b) = tuple { ... }
However, I'm not sure if Swift will have pinned down the type of the initializer at the point where it's generating the pattern. If this would be difficult or impossible to implement, Swift should continue to interpret code like this as attempting to bind an optional tuple, rather than a tuple of optionals.
In theory, Swift could allow you to bind a tuple of optionals to a single constant:
if let tuple = (opt1, opt2) { ... }
However, this seems error-prone; the pattern doesn't draw a very clear picture of the value being operated upon, so you could easily misinterpret it as binding an optional tuple. Because of this ambiguity, Swift should always interpret this construct as binding an optional tuple, rejecting it with a type error if necessary.
This proposal is additive compared to SE-0099, but in combination with it, essentially replaces the Swift 2.2 compound binding syntax with a different, incompatible one. When moving directly from Swift 2.2, the migrator should convert old-style compound optional binding clauses:
guard let a = opt1, b = opt2, c = opt3 else { ... }
Into the new, tuple-based ones:
guard let (a, b, c) = (opt1, opt2, opt3) else { ... }
The "one-let
-per-binding" syntax remains compatible with both Swift
2.2 and Swift 3, so projects which must support both can still perform
multiple optional bindings in a single if
statement without
resorting to an #if swift(>=3.0)
build configuration:
guard let a = opt1, let b = opt2, let c = opt3 else { ... }
This proposal does not add new functionality; it merely removes keyword clutter. However, it offers a convenient replacement for a commonly-used feature which has just been removed as a result of grammatical ambiguity, not user confusion or lack of utility.
However, all of optional binding is redundant with case conditions;
we keep it anyway because it's a convenient shorthand and saves
beginners from having to learn about pattern matching. Multiple
bindings are a natural fit for the subset of case
features available
through optional binding.
Rather than including this functionality in the compiler, the standard library could provide a series of functions like:
public func all<T, U>(_ t: T?, _ u: U?) -> (T, U)? { ... }
public func all<T, U, V>(_ t: T?, _ u: U?, _ v: V?) -> (T, U, V)? { ... }
// etc.
These could then be used in a similar fashion to this proposal:
guard let (a, b, c) = all(opt1, opt2, opt3) else { ... }
However, because we do not have variadic generics, we would need to
provide a set of overloads for different arities, and our support
would be limited to the arities we chose to provide. (Support for
tuples, as opposed to separate parameters, would require a second set
of overloads). Meanwhile, the tuple matching syntax is already
precedented in case
conditionals, so extending it seems pretty
natural. Providing this in the compiler seems like the right solution.
Im surprised this is not already in swift 3