- Proposal: SE-TBD
- Author(s): Ben Cohen, Erica Sadun, Paul Cantrell and several other folk
- Status: tbd
- Review manager: tbd
This proposal introduces an annotated forced unwrapping operator to the Swift standard library, completing the ?
, ??
, and !
family with !!
. The "unwrap or die" operator provides text feedback on failed unwraps, to support defensive programming. This operator is commonly implemented in the larger Swift Community and should be considered for official adoption.
This proposal was first discussed on the Swift Evolution list in the [Pitch] Introducing the "Unwrap or Die" operator to the standard library thread.
Forced unwraps are an ugly but essential part of the Swift programming language. Their use is strengthened by providing a documentary "guided landing" that explains why the unwrapping was guaranteed to be non-nil. Existing Swift constructs (like precondition
and assert
) incorporate meaningful strings for output when the app traps:
assert(!array.isEmpty, "Array guaranteed to be non-empty because...")
let lastItem = array.last!
guard !array.isEmpty
else { fatalError("Array guaranteed to be non-empty because...") }
// ... etc ...
Guard statements, assertions, preconditions, and fatal errors allow developers to backtrack and correct assumptions that underlie their design. When your application traps because of an unwrapped nil, debug console output explains why straight away. You don't have to hunt down the code, read the line that failed, then establish the context around the line to understand the reason. This is even more important when you didn’t write this code yourself.
Incorporating string based rationales explains the use of forced unwraps in code, providing better self documentation of facts that are known to be true. The strings support better code reading, maintenance, and future modifications.
When an optional can a priori be guaranteed to be non-nil, using guards, assertions, preconditions, etc, are relatively heavy-handed approaches to document these established truths. When you already know that an array is not-empty, you should be able to specify this in a single line using a simple operator:
// Existing force-unwrap
let lastItem = array.last! // Array guaranteed to be non-empty because...
// Proposed unwrap operator with fallback explanation
let lastItem = array.last !! "Array guaranteed to be non-empty because..."
This proposal introduces an "unwrap or die" operator that documents the reason a forced unwrap that should always succeed has failed. Adopting this operator:
- Encourages a more controlled approach to unwrapping,
- Promotes documenting reasons for trapping during unwraps, and
- Provides a succinct and easily-taught form for new Swift learners.
An often-touted misconception exists that force unwraps are, in and of themselves, bad: that they were only created to accommodate legacy apps, and that you should never use force-unwrapping in your code. This isn’t true.
There are many good reasons to use force unwraps, though if you're often reaching for it, it’s generally a bad sign. Force-unwrapping can be a better choice than throwing in meaningless default values or applying optional chaining when the presence of nil would indicate a serious failure.
Introducing the !!
operator endorses and encourages the use of "thoughtful force-unwrapping". It incorporates the reason why the forced unwrap should be safe (for example, why the array can’t be empty at this point, not just that it is unexpectedly empty). If you’re already going to write a comment, why not make that comment useful for debugging at the same time?
Using !!
provides syntactic sugar for the following common unwrap pattern:
guard let y = x
else { fatalError("reason") }
// becomes
let y = x !! "reason"
// and avoids
let y = x! // reason
Although comments document in-code reasoning, these explanations are not emitted when the application traps on the forced unwrap:
As the screener of a non-zero number of radars resulting from unwrapped nils, I would certainly appreciate more use of
guard let x = x else { fatalError(“explanation”) }
and hope that!!
would encourage it.
Sometimes it’s not necessary to explain your use of a forced unwrap. In those cases the normal !
operator will remain, even after the introduction of !!
. You can continue using !
, as before, just as you can leave off the string from a precondition.
Although burning a new operator is a serious choice, !!
is a good candidate for adoption:
- It matches and parallels the existing
??
operator. - It fosters better understanding of optionals and the legitimate use of force-unwrapping in a way that encourages safe coding and good documentation, both in source and at run-time.
!!
sends the right semantic message. It communicates that "unwrap or die" is an unsafe operation and that failures should be both extraordinary and explained.
The new operator is consciously based on !
, the unsafe forced unwrap operator, and not on ??
, the safe fallback nil-coalescing operator. Its symbology therefore follows !
and not ?
.
infix operator !!: NilCoalescingPrecedence
extension Optional {
/// Performs a forced unwrap operation, returning
/// the wrapped value of an `Optional` instance
/// or performing a `fatalError` with the string
/// on the rhs of the operator.
///
/// Forced unwrapping unwraps the left-hand side
/// if it has a value or errors if it does not.
/// The result of a successful operation will
/// be the same type as the wrapped value of its
/// left-hand side argument.
///
/// This operator uses short-circuit evaluation:
/// The `optional` lhs is checked first, and the
/// `fatalError` is called only if the left hand
/// side is nil. For example:
///
/// guard !lastItem.isEmpty else { return }
/// let lastItem = array.last !! "Array guaranteed to be non-empty because..."
///
/// let willFail = [].last !! "Array should have been guaranteed to be non-empty because..."
///
///
/// In this example, `lastItem` is assigned the last value
/// in `array` because the array is guaranteed to be non-empty.
/// `willFail` is never assigned as the last item in an empty array is nil.
/// - Parameters:
/// - optional: An optional value.
/// - message: A message to emit via `fatalError` after
/// failing to unwrap the optional.
public static func !!(optional: Optional, errorMessage: @autoclosure () -> String) -> Wrapped {
if let value = optional { return value }
fatalError(errorMessage())
}
}
With one notable exception, the !!
operator should follow the same semantics as Optional.unsafelyUnwrapped
, which establishes a precedent for this approach:
"The unsafelyUnwrapped property provides the same value as the forced unwrap operator (postfix !). However, in optimized builds (-O), no check is performed to ensure that the current instance actually has a value. Accessing this property in the case of a nil value is a serious programming error and could lead to undefined behavior or a runtime error."
By following Optional.unsafelyUnwrapped
, this approach is consistent with Swift's error handling system:
"Logic failures are intended to be handled by fixing the code. It means checks of logic failures can be removed if the code is tested enough. Actually checks of logic failures for various operations,
!
,array[i]
,&+
and so on, are designed and implemented to be removed when we use-Ounchecked
. It is useful for heavy computation like image processing and machine learning in which overhead of those checks is not permissible."
Like assert
, unsafelyUnwrapped
does not perform a check in optimized builds. The forced unwrap !
operator does, like precondition
. The "unwrap or die" !!
operator should likely behave like precondition
, not assert
to preserve trapping information in optimized builds.
Unfortunately, there is no direct way at this time to emit the #file
name and #line
number with the above code. We hope the dev team can somehow work around this limitation to produce that information at the !!
site.
Here are a variety of examples that demonstrate the !!
operator in real-world use:
// In a right-click gesture recognizer action handler
let event = NSApp.currentEvent !! "Trying to get current event for right click, but there's no event”
// In a custom view controller subclass that only
// accepts children of a certain kind:
let existing = childViewControllers as? Array<TableRowViewController> !! "TableViewController must only have TableRowViewControllers as children"
// Providing a value based on an initializer that returns an optional:
lazy var emptyURL: URL = { return URL(string: “myapp://section/\(identifier)") !! "can't create basic empty url” }()
// Retrieving an image from an embedded framework:
private static let addImage: NSImage = {
let bundle = Bundle(for: FlagViewController.self)
let image = bundle.image(forResource: "add") !! "Missing 'add' image"
image.isTemplate = true
return image
}()
// Asserting consistency of an internal model:
let flag = command.flag(with: flagID) !! "Unable to retrieve non-custom flag for id \(flagID.string)"
// drawRect:
override draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext() !! "`drawRect` context guarantee was breeched"
}
The !!
operator generally falls in two groups:
-
Asserting System Framework Correctness: The
NSApp.currentEvent
property returns anOptional<NSEvent>
as there’s not always a current event going on. It is always safe to assert an actual event in the action handler of a right-click gesture recognizer. If this ever fails,!!
provides an immediately and clear description of where the system framework has not worked according to expectations. -
Asserting Application Logic Correctness: The
!!
operator ensures that outlets are properly hooked up and that the internal data model is in a consistent state. The related error messages explicitly mention specific outlet and data details.
These areas identify when resources haven't been added to the right target, when a URL has been mis-entered, or when a model update has not propagated completely to its supporting use. Incorporating a diagnostic message, provides immediate feedback as to why the code is failing and where.
In one real-world case, a developer's deployed code crashed when querying Apple's smart battery interface on a Hackintosh. Since the laptop in question wasn’t an actual Apple platform, it used a simulated AppleSmartBatteryManager interface. In this case, the simulated manager didn’t publish the full suite of values normally guaranteed by the manager’s API. The developer’s API-driven contract assumptions meant that forced unwraps broke his app:
Since IOKit just gives you back dictionaries, a missing key, is well… not there, and nil. you know how well Swift likes nils…
Applications normally can’t plan for, anticipate, or provide workarounds for code running on unofficial platforms. There are too many unforeseen factors that cannot be incorporated into realistic code that ships. Adopting a universal "unwrap or die" style with explanations enables you to "guide the landing" on these unforseen "Black Swan" failures:
guard let value = dict[guaranteedKey]
else {
fatalError("Functionality compromised when unwrapping " +
"Apple Smart Battery Dictionary value.")
return
}
// or more succinctly
let value = dict[guaranteedKey] !! "Functionality compromised when unwrapping Apple Smart Battery Dictionary value."
The !!
operator reduces the overhead involved in debugging unexpected Black Swan deployments. This practice adds robustness and assumes that in reality bad execution can happen for the oddest of reasons. Providing diagnostic information even when your assumptions are "guaranteed" to be correct is a always positive coding style.
Right now, fatalError
reports the line and file of the !!
operator implementation rather than the code where the operator is used. At some point Swift may allow operator implementations with more than two parameters. At such time, the !!
operator should incorporate the source line and file of the forced unwrap call:
public static func !!(optional: Optional, errorMessage: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> Wrapped
This could be a minimal modification to lib/AST/Decl.cpp:4919, to the FuncDecl::isBinaryOperator()
implementation, enabling size() > 2
if get(2+)->isDefaultArgument()
:
bool FuncDecl::isBinaryOperator() const {
if (!isOperator())
return false;
auto *params = getParameterList(getDeclContext()->isTypeContext());
return params->size() == 2 &&
!params->get(0)->isVariadic() &&
!params->get(1)->isVariadic();
}
Having a line and file reference for the associated failure point would be a major advantage in adopting this proposal, even if some under-the-covers "Swift Magic™" must be applied to the !!
implementation.
If Never
ever becomes a true bottom type as in SE-0102, Swift will be able to use fatalError()
on the right hand side of nil-coalescing.
// Legal if a (desirable) `Never` bottom type is adopted
let x = y ?? fatalError("reason")
This proposal supports using a String
(or more properly a string autoclosure) on the rhs of a !!
operator in preference to a Never
bottom type or a () -> Never
closure with ??
for the reasons that are enumerated here:
-
A string provides the cleanest user experience, and allows the greatest degree of in-place self-documentation.
-
A string respects DRY, and avoids using both the operator and the call to
fatalError
orpreconditionFailure
to signal an unsafe condition:let last = array.last !! "Array guaranteed non-empty because..." // readable
versus:let last = array.last !! fatalError("Array guaranteed non-empty because...") // redundant
-
A string allows the operator itself to unsafely fail, just as the unary version of
!
does now. It does this with additional feedback to the developer during testing, code reading, and code maintenance. The string provides a self-auditing in-line annotation of the reason why the forced unwrap has been well considered, using a language construct to support this. -
A string disallows a potentially unsafe
Never
call that does not reflect a serious programming error, for example:let last = array.last !! f() // where func f() -> Never { while true {} }
-
Using
foo ?? Never
requires a significant foundational understanding of the language, which includes a lot of heavy lifting to understand how and why it works. Using!!
is a simpler approach that is more easily taught to new developers: "This variation of forced unwrap emits this message if the lhs is nil." -
Although a
Never
closure solution can be cobbled together in today's Swift,!!
operator solution can be as well. Neither one requires a fundamental change to the language.
Pushing forward on this proposal does not in any way reflect on adopting the still-desirable Never
bottom type.