SE-0217 - The "Unwrap or Die" operator

If we support operators expressing underlying functions, is it possible to implement an operator that relays to a function with the following signature: (lhs: T, rhs: U, file: StaticString = #file, line: UInt = #line) -> V, and then provide an operator association attribute @operator(valid_operator, fixity: infix)?

We already introduced use cases from code from a variety of developers into the proposal. The operator's use in the wild, to be clear, drove this proposal, in a variety of domains from Cocoa to iOS. Upthread, uses with C APIs were discussed by Jacob Williams. I don't believe the proposal is lacking a breadth of use-cases. Any developer who may experience an unexpected failure related to unwrapping an optional, who wishes to emit diagnostic information about that unexpected failure, is a candidate for this solution regardless of whether iOS, server side, system tool, C interop, etc.

Focusing on the fixit, which I know you specifically asked to be removed, may be pulling this discussion off-course as with any text relating to pedagogy, even though learning and naive users are major drivers of the problem domain.

That the solution has not been generalized to try! or as! does not mean this issue is not an important one that should be addressed. I believe this loss of focus is due to a fundamental issue of language expressibility and diagnostics. I would like to see operator-nonoperator coupling discussed further. I'd like to see fixits discussed further. I would like to see better diagnostics for try! and as!. But for now, I'd like to see a commitment to a solution to this particular problem: the unexpected deployment catastrophe encountered when unwrapping an optional that is otherwise guaranteed to hold a value.

7 Likes

Erica, looking back at our proposal, you’re quite right — there are plenty of use cases.

@Chris_Lattner3, per your wondering about support for try! and as!, I did a highly unscientific scan of the Swift compat suite, and came up with these estimates of how frequently they’re used relative to force unwrapping:

operation count
force unwrap ~2000
as! 394
try! 107

I excluded test suites from that count, because tests tend to use force unwrapping much more frequently (and because usage in tests tends to be a form of asserting expected results, and thus not to call for the same kind of explanatory comments this proposal is concerned with).

A skim of ! usage suggests it’s generally well motivated. I came across many examples where either (1) restructuring code would eliminate it, or (2) there’s probably a lurking bug. (Alamofire was notably loose with its use of force unwrapping.) However, in the solid majority of cases, usage of force unwrapping seemed justifiable. In many cases, developers already added comments of exactly the kind we're trying to encourage here. In a few, I wished they had.

7 Likes

To be clear, I'm speaking for myself only, the core team hasn't even spoken about this proposal yet:

Erica (and Paul, and others). I understand that you have a pragmatic goal here, and I totally respect your focus on solving specific user challenges. I really do get that, and I really understand where you are coming from. The concerns I'm raising are not from lack of interest. Here is a more detailed explanation of my position:

Swift is still a "young" language. It is missing key (large) pieces of functionality, including a baked resilience model, ownership model, concurrency model and reliability model. These are huge bricks that should make up the house of Swift, and each will introduce new complexity into the model. We (as a community) will do our best to factor that complexity and progressively disclose it, but these are all large things that have significant unknowns, which really do need to happen.

At the same time, the swift-evolution process naturally encourages smaller incremental improvements that are "gap fillers". These are really important to me because (for example) the standard library is missing a bunch of fairly obvious convenience functions, and I'm thrilled to see those get added.

At the same time, "gap filling" when it comes to core syntax and core syntactic sugar is really dangerous right now. Operators have global impact on the language and should only be added when there is a very strong rationale for doing so. In contrast, methods only affect the types they apply to, and deprecating them is not a big deal (having a deprecated operator in the core language would be pretty embarrassing).

Again, I understand your pragmatic goal, but this proposal is problematic to me for several reasons. Because it only applies to x! it undermines a core orthogonality of ! that we've fought to preserve (remember how controversial try! was?). This proposal makes swift more complicated not only because it introduces an operator, but also because that operator is a special case that breaks a unifying principle in the language (it also doesn't chain/compose well as I've mentioned, but that's a more minor detail).

Furthermore, this proposal is likely to be subsumed in the future by more general functionality. I keep mentioning a scoped failure handler approach because the reliability model is very likely to include something like it.

In short, I think it would be very unfortunate to introduce new core syntax for this, particularly given that it makes the language less orthogonal, does not solve the whole problem, and which may become legacy in ~two years. We've made it this far without this, and while I understand that your goals are well-meaning and pragmatic, I don't see the rush to make such a high impact change here.

To be clear, I'm not trying to impede progress, and I have given a different approach to consider: introducing a new method. @davedelong mentioned that we could introduce an ".unwrap()" method, make x! be sugar for it (at least when it is an rvalue) and then introduce an .unwrap(message: ) form. Such an approach solves the same problem as this proposal, is not invasive into the core language feel, and chains more nicely. I would love to see a discussion about why this approach is problematic.

-Chris

27 Likes

I’m minor -1 on this proposal. I would like to see a redesigned FatalError with built-in options for common error cases to come along with this proposal. For example, let x = URL("str") !! Fatal.invalidLiteralValue. Having to provide a custom string for everything seems cumbersome to me.

I'd really love to see this happen, for the record! It seems like exactly the right solution for server-side code. Do you have any kind of idea if and when something like this is going to land? Currently, this is a huge gap.

Different pieces will land at different times, but it is likely to take several years for the whole plan to come together (and it is likely to evolve and change compared to what the manifesto lays out).

-Chris

1 Like

I don't understand why you think unwrap(message:) would be problematic. Could you explain further?

Sorry for the confusion, I'm saying that I would prefer an approach like this.

I don't see why x.unwrap("reason why it shouldn't be nil") is worse than x !! "reason why it shouldn't be nil".

-Chris

6 Likes

That works for me too. I want a solution, not a design and not thermonuclear global war. It needs an external label: x.unwrap(because: "reason why it shouldn't be nil") and if we're going there, would you please read through [Proposal] Introducing Namespacing for Common Swift Error Scenarios by erica · Pull Request #861 · apple/swift-evolution · GitHub ? They're tightly coupled. We want to have central, extensible, reusable fatal messages meant for emitting.

That said, this is @Ben_Cohen and @davedelong's baby. Ben, Dave, what are your thoughts? @Paul_Cantrell?

(Also, since I'm already off topic mentioning namespaced fatal outcomes, would you guys please look at raw strings. Thanks.)

3 Likes

Would you by the same logic be for replacing the force unwrap operator by the unwrap method?

One reason I can think of – though possibly not enough of one to prefer the operator version:

My experience with Optional.map is that methods on optional itself can cause confusion. It often takes a leap of understanding to realize that Optional<String> can actually have methods of its own, distinct from String. Eventually people get it, and it's not such a big deal with map because it's more of an "advanced" feature. But if unwrap were something we wanted even beginners to use, it could be an issue.

7 Likes

Fine by me. Not sure I’m sold on that exact naming/wording, but I like the general idea.

I’d also thought Chris was keen on a solution that would handle try! and as!, and spent some time exploring alternate spellings of his scoped message idea:

perform(URL(string: ":")!, assuming: "a single colon is a valid URL")

failableOperation(try! subject.value(), isSafeBecause: "subject can't error out or be disposed")

return _when([pu.asVoid(), pv.asVoid()]).map(on: nil) {
    assuming("both promises are now completed and have values") {
        (pu.value!, pv.value!)
    }
}

Again, exact naming is highly debatable, but this seems like a promising direction.

(Note that ! still does the force unwrapping in the examples above. The idea here is that you use existing ! / try! / as!, and this new function — whatever it’s called — attaches the given message to any crash that occurs within the wrapped expression. This doesn’t represent any kind error handling or recovery behavior; it’s is still just about adding a message to the failure.)

I’m fine with either direction. I also agree that there are bigger problems to solve. If one of these approaches can find general support and be an easy win, I’m all for it.

It's also worth pointing out that methods on Optional actually don't interact very well with chaining. You have to use parentheses around anExpression?.involvingOptional?.chains if you want to (invoke?.a).methodOnOptionalItself(). I'm not sure which variation would require parentheses more frequently but when the unwrap subexpression appears a the right of the expression the operator variation would be substantially more convenient to use. I suspect the use cases are heavily weighted in this direction.

3 Likes

I think removing the ! operator would be a very bad idea. It signals that this could be dangerous and is also a part of other operators like: as! and try!.

If you write invoke?.someProperty, then someProperty is not optional anymore because you unwrap value with ?, then adding parentheses turns it into expression with optional result, which means you can use methods from Optional. This is equal to the next lines:

let someProperty = invoke?.someProperty // someProperty is Optional<SomePropertyType>
let resultOfOptionalManipulation = someProperty.methodOnOptionalItself()

But if you will write without unwrapping using ? you can call optional methods on invoke, because it is optionally, e.g.:

let resultOfOptionalManipulation = invoke.methodOnOptionalItself()

Which seems for me to be an expected behavior.

Yes it is. The reason I pointed it out is because Chris argued that the method interacts better with chaining. It may in some contexts, but not in the context where the unwrap needs to happen at the end of a chain involving optional. In that context the operator interacts better with chaining.

I’m not arguing one way or the other here, just pointing out this difference for everyone to consider.

Please make optional.unwrap() unwrap all the nested optionals. This would be such a great win!

Now that I think about I think I would have expected !! to doubly unwrap.

Responding to points above:

  • Sure, I'd be +1 on having a foo.unwrap() method as an analog for foo!. We are clearly not going to remove foo! though. The point would be to establish a consistent naming and have a story to relate these things.

  • I agree the method doesn't solve the try! cases etc. The major reason it is better than the !! operator is that it doesn't undermine consistency we have of the ! sigil, by not using it. It is also preferable to me because it is less invasive on the core language syntax.

  • I agree that a scoped approach like Paul is exploring is also interesting, but it is more invasive because it requires some amount of runtime integration. This makes it more difficult to scope and land as a short term "pragmatic solution" to today's problems.

  • I think the chaining point I made above is a weak/auxiliary point, not a core point. try! and as! aren't great at chaining either.

Exactly! — operators are just the most prominent Swift-way of handling Optional. What we are addressing here is prevalent misuse of the force unwrap operator. We need to recognize that it stems from its ultimate convenience. Language already contains many tools for safely handling optionals: if-let, guard-let, etc. But these multi-line expressions are just too heavyweight to compete with !. The correct way to deal with the problematic situation is too inconvenient when compared with the ease of adding !. It is just too attractive. Chris suggests an alternative single line solution:

We need to acknowledge that a method call is still one level less convenient than an operator. To see why it isn’t easy enough, just reverse the situation: Imagine you had to always write .unwrap() instead of !. But there is operator !! "Precondition documentation". Does this tip the balance towards more thoughtful use of force unwrapping? Now reevaluate this in our reality, where ! is here to stay. The countermeasure for ! misuse must be as easy as possible to be effective in practice.

2 Likes

I agree that both of these issues are significant. If we wanted to use something other than an operator, I would prefer a precondition<T>(_ value: T?, _ message: String) -> T function to an Optional.unwrap(_:) method.

But I still prefer !! to either of these. If we want people to document the safety of most force-unwraps, convenience really matters, and I doubt whatever hypothetical mechanism we might invent in the future to handle traps will be nearly as convenient as !!. And while it's unfortunate that !! has no parallel for as! and try!, I don't see an elegant way to cover all three without a lot of additional syntactic overhead and a lot of additional concepts for people to learn and juggle.

1 Like