Unwrapping values is one of the most common Swift tasks and it is unnecessarily complex.
Consider the following solutions:
- Introduce an
unwrap
keyword forOptional
values - Introduce an
Unwrappable
protocol for associated-value enumerations. - Apply
unwrap
to non-Optional
values. - Extend
for
andswitch
. - Fix pattern match binding issues.
- Simplify complex binding.
Unwrapping with conditional binding and pattern matching is unnecessarily complex and dangerous:
- Using "foo = foo" fails DRY principles.
- Using
case let .some(foo) = foo
orcase .some(let foo) = foo
fails KISS principles. - Using the
=
operator fails the Principle of Least Astonishment. - Allowing user-provided names may shadow existing variables without compiler warnings.
- The complexity and freedom of
let
andvar
placement can introduce bugs in edge cases.
The following code fails DRY:
guard let foo = foo else { ... } // redundant
The following code fails KISS:
guard case let .some(foo) = foo else { ... } // overly complex
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) =
performs conditional binding on enumerations and applies pattern matching
These two statements are functionally identical:
if range ~= myValue { ... } // simpler
if case range = myValue { ... } // confusing
guard case
and if case
look like standard non-conditional assignment statements but they are not assignment statements.
Further:
- In
switch
, acase
is followed by a colon, not an assignment operator. - Swift has a pattern matching operator (
~=
) but does not use it here.
The following code shadows an existing symbol, since the unwrapped version does not constrain itself to a same-name convention:
if let existingSymbol = optionalSymbol { ... }
Using let
externally can introduce bugs in outlier edge conditions when simultaneously pattern matching and performing variable binding. The following example demonstrates this issue.
Consider the following enumeration and values:
// An enum with one, two, or three associated values
enum Value<T> { case one(T), two(T, T), three(T, T, T) }
// An example with two associated values
let example2: Value<Character> = .two("a", "b")
// A bound symbol
let oldValue = "x"
This code's goal is to conditionally bind newValue
and pattern match the value stored in the oldValue
symbol. The first example succeeds. The second example compiles and runs but does not match the coder's intent. Using an external let
creates a new oldValue
shadow instead of pattern matching oldValue
's stored value:
// Safe
if case .two(let newValue, oldValue) = example2 {
...
}
// Syntactically legal but incorrect
if case let .two(newValue, oldValue) = example2 {
...
}
The following solutions address the issues discussed in the previous section.
Introducing an unwrap
keyword produces a shadowed same-name variable in condition clauses like those used in guard
and if
statements.
guard unwrap foo else { ... } // simpler
guard unwrap var foo else { ... } // variable version
guard unwrap let foo else { ... } // for language consistency
This simplifies the status quo and eleminates unintended shadows
// 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
Using 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. The unwrap
keyword is common, simple to understand, and easy to search for if Swift users are unfamiliar with it.
Optional magic should not be limited to the Optional
type. An Unwrappable
protocol allows associated-type enumerations to support unwrapping beyond the Optional
type. This opens the door to a better way to introduce biased-value enumerations. Biased-value refers to an associated type enumeration that has a natural "success" underlying type, which may or may not be limited to a single case.
Removing unwrapping support from the compiler into the standard library enables Swift to simplify compiler details and unify optional-specific behaviors like chaining, nil-coalescing (??
), if let, forced unwrap (!
),
etc, enabling their repurposing for additional types as well as optionals.
Using a protocol for Unwrappable instead of a single generic enum or struct (as with Optional) supports more complex cases and enhances overall flexibility.
This protocol design allows a conforming type to unwrap a single associated value type, which can be used in one or more "success" cases:
protocol Unwrappable {
associatedtype BoxedValue
func unwrap() -> BoxedValue?
}
Unwrappable types declare their preference for a single unwrapped type like the some case for Optional and the success case in a (yet to be designed) Result
type. Adopting Unwrappable
allows conforming types to inherit many behaviors currently limited to optionals.
Conforming Result to Unwrappable would enable you to introduce if let binding and functional chaining that checks for and succeeds with a success case (type T
) but otherwise discards and ignores errors associated with failure (conforming to Error
). Unwrappable
allows you to shortcut, coalesce, and unwrap-to-bind as you would an Optional
, as there's always a single favored type to prefer.
For example:
public enum Result<T> {
case success(value: T)
case failure(error: Error)
}
public extension Result {
public typealias BoxedValue = T
public func unwrap() -> T? {
guard case success(let value) = self
else { return nil }
return value
}
}
public enum Either<Left, Right> {
case left(Left)
case right(Right)
}
public extension Either: Unwrappable {
public typealias BoxedValue = Right
public func unwrap() -> Right? {
guard case right(let value) = self
else { return nil }
return value
}
}
public enum EitherOr<T> {
case left(T), right(T)
}
public extension EitherOr: Unwrappable {
public typealias BoxedValue = T
public func unwrap() -> T? {
switch self {
case left(let value): return value
case right(let value): return value
}
}
}
Unwrappable enables code to ignore and discard an error associated with failure, just as you would when applying try? to convert a thrown value to an optional. The same mechanics to apply to Optional would apply to Result
including features like optional chaining, forced unwrapping and so forth.
The design of Unwrappable
should allow an unpreferred error case to throw on demand and there should be an easy way to convert a throwing closure to a Result.
// Any throwing call or block can become a `Result`:
let myResult = Result(of: try myThrowingCall())
// Later (this could even be in a different thread)
let value = try unwrap myResult // throws on `failure`
If unwrap
is adopted for non-Optionals, valid code might include:
switch result {
case eitherInstance: ...
case .left(let value) where value < 10: ...
case unwrap value where value > 100: ...
...
}
for unwrap value in resultsArray {
...
}
Simultaneously unwrapping and pattern matching can introduce errors by introducing a new shadow instead of matching an existing symbol.
...oldValue is bound...
// Syntactically legal but incorrect
// The intent is to bind
if case let .two(newValue, oldValue) = example2 {
...
}
To prevent this, Swift can:
- Emit a warning when shadowing a bound symbol during pattern matching; or
- Disallow external
let
/var
, moving their use to each site.
See SR-4174.
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
Dropping the case
keyword and replaces =
with ~=
simplifies pattern matching for non-Unwrappable
values. The results look like this, showcasing a variety of let placement, variable binding, and optional sugar alternatives.
guard .success(let value, _, _) ~= result else { ... } guard .success(var value, _, _) ~= result else { ... } if .success(let value, _, _) ~= result { ... } if .success(var value, _, _) ~= result { ... }
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
Some list participants have requested values on the lhs, patterns on the rhs. Using =~ instead of ~= could support reversed layout.
Re: Apply Unwrap to Non-optional Values, I don't know if this is a good idea, because the types may be implicit. Someone may not know the variable contains a Result and not an Optional.
For result, if unwrap result would make it very difficult for someone reading the code to understand that there is potentially an error being ignored when the block skips.
For an Either type, this makes it unclear that there may be another value that is being ignored.
Of course, without Result and Either implementing Unwrappable, there isn't really a reason to have the protocol