- Proposal: TBD
- Author: Erica Sadun, Chris Lattner, Xiaodi Wu David Goodine
- Status: TBD
- Review manager: TBD
This proposal redesigns common unwrapping tasks:
- It introduces the
unwrap
keyword for optional values - It re-architects
guard case
andif case
grammar to support unwrapping more complex enumerations by dropping thecase
keyword and replacing=
with~=
. - It applies
unwrap
to non-optional values
Swift Evolution threads:
Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex. This proposal simplifies this process and enhances code safety and readability.
Swift lacks a safe way to bind an optional to a shadowed same-name variable in condition clauses like those used in guard
and if
statements.
Compare:
guard let foo = foo else { ... } // redundant
guard case let .some(foo) = foo else { ... } // overly complex
guard unwrap foo else { ... } // simpler
- Using "foo = foo" fails DRY principles.
- Using
case let .some(foo) = foo
orcase .some(let foo) = foo
fails KISS principles.
unwrap
guarantees that an unwrapped shadow uses the same name as the wrapped version. This ensures that a conditionally-bound item cannot accidentally shadow another symbol. It eliminates repetition and retains clarity. Further, the unwrap
keyword is common, simple to understand, and easy to search for if Swift users are unfamiliar with it.
In the rare case that the binding is variable, use the re-imagined var ~=
syntax.
Swift's guard case
and if case
statements stand out for their unintuitive approach. They look like assignment statements but they are not assignment statements. This violates the principle of least astonishment. This presents difficulties for new language adopters by combining several concepts in a confusing form. They are arguably underutilized by language experts.
The status quo for the =
operator is iteratively built up in this fashion:
=
performs assignmentlet x =
performs bindingif let x =
performs conditional binding on optionalsif case .foo(let x) =
andif case let .foo(x) =
performs conditional binding on enumerations and applies pattern matching
Both guard case
and if case
statements perform simultaneous pattern matching and conditional binding. Here are examples demonstrating their use in current Swift:
enum Result<T> { case success(T), error(Error) }
// valid Swift
guard case let .success(value) = result
else { ... }
// valid Swift
guard case .success(let value) = result
else { ... }
The problems with guard case
and if case
include:
- The
=
operator looks like assignment and not like pattern matching (~=
). - The
case
layout is both too close to aswitch
'scase
but doesn't follow its syntax. Inswitch
, acase
is followed by a colon, not an equal sign. - Using the
case
syntax is unneccessarily wordy. It incorporatescase
,=
, and optionallylet
/var
assignments.
Swift uses two kinds of pattern matching.
Indirect pattern matching such as the kind you see in switch
and for
statements receives an argument in from the statement structure. The argument is not mentioned directly in the case
:
switch value {
case .foo(let x): ... use x ...
...
}
for case .foo(let x) in value { ... }
Direct pattern matching including guard
/if
statements and with the pattern matching operator place the argument to be matched to the right of an operator, either =
or ~=
. The argument is explicitly mentioned:
if case .foo(let x) = value { ... use x ... }
if 100...200 ~= value { ... }
When using if case
/guard case
in the absence of conditional binding, statements duplicate basic pattern matching with less obvious semantics. These following two statements are functionally identical. The second uses an assignment operator and the case
keyword.
if range ~= value { ... } // simpler
if case range = value { ... } // confusing
This proposal introduces the unwrap
keyword. The unwrap
statement shadows an enumeration variable to an unwrapped version of the same type. Upon adopting this proposal the following statements produce equivalent behvior:
// New unwrap keyword
if unwrap value { ... }
// Existing same-name shadowing
if let value = value { ... }
// Existing same-name pattern matching and conditional binding
if case .some(let value) = value { ... } // old grammar
if case let .some(value) = value { ... } // old grammar
// Proposed same-name pattern matching and conditional binding
if .some(let value) ~= value { ... } // new grammar
if let .some(value) ~= value { ... } // new grammar
In if case
and guard case
, this proposal drops the case
keyword and replaces the equal sign with the pattern matching operator. The results look like this:
guard let .success(value) ~= result else { ... }
guard .success(let value) ~= result else { ... }
if let .success(value) ~= result { ... }
if .success(let value) ~= result { ... }
guard let x? ~= anOptional else { ... }
if let x? ~= anOptional { ... }
Users may choose to use var
instead of let
to bind to a variable instead of a constant.
In this update:
- The
case
keyword is subsumed into the (existing) pattern matching operator - The statements adopt the existing
if-let
andguard-let
syntax, includingOptional
syntactic sugar.
if let x = anOptional { ... } // current
if case let x? = anOptional { ... } // current, would be removed
if let x? ~= anOptional { ... } // proposed replacement for `if case`
On adopting this syntax, the two identical range tests naturally unify to this single version:
if range ~= value { ... } // before
if case range = value { ... } // before
if range ~= value { ... } // after
Using pattern matching without conditional binding naturally simplifies to a standalone Boolean condition clause.
Real world Swift enumerations rarely follow the Optional pattern, which can be summed up like this:
enum OptionalLike<T> { case aCaseWithOneAssociatedValue(T), anotherCase }
They more typically look like this:
// One generic, one known type
enum Result<Value> { case success(Value), failure(Error) }
// Many cases of mixed types
enum JSON {
case string(String)
case number(Double)
case boolean(Bool)
case array([JSON])
case dictionary([String: JSON])
case null
}
// Multiple associated values
enum Response { case contact(code: Int, message: String), failure }
You can adapt the bind
keyword to work with these real-world cases in one of two ways. The first way uses unwrap
instead of let
or var
. Here are a few varieties of how that call might look versus the proposed update for normal pattern matching:
if unwrap .string(myString) ~= json { ... }
if unwrap .contact(code, _) ~= response { ... }
if unwrap .contact(code, var message) ~= response { ... }
// vs proposed
if let .string(myString) ~= json { ... }
if var .string(myString) ~= json { ... }
if .contact(let code, _) ~= response { ... }
if .contact(let code, var message) ~= response { ... }
Although slightly wordier than let
and var
, the unwrap
solution offers advantages:
- It enhances readability. If the goal is to unwrap an embedded value,
unwrap
uses a more appropriate term. - It establishes one place for the keyword to live instead of the "does it go inside or outside" status quo. A consistent place means prettier code.
- As shown in these examples, it can easily be adapted for variable binding. If you want to override the let behavior, you can insert a var inside the parentheses.
A second, riskier, cleaner approach looks like this. It assumes the compiler can pick up and shadow using either the associated value labels (preferred) or the enumeration name (as you'd see with String
raw value enumerations):
if unwrap .contact ~= response {
... some compiler magic picks up on the `code` and `message` labels
used for initialization, so the bound variables are `code`
and `message`
}
if unwrap .string ~= json {
... use `string` here (same name as enum case) because
no label was used to define the associated value
}
If unwrap
is adopted for non-Optionals, valid code might include:
switch result {
case .contact(0, _): ...
case let .contact(code, message) where code < 10: ...
case unwrap .contact(code, message) where code > 100: ...
...
}
for unwrap .string(aString) in jsonArray {
...
}
This proposal is breaking and would require migration.
- Leaving the grammar as-is.
- Retaining
case
and replacing the equal sign with~=
(pattern matching) or:
(to match the switch statement).