SE-0217 - The "Unwrap or Die" operator

First of all, this review thread is a trainwreck. I hope the Core Team remains committed to fostering a positive community. They shouldn’t feel alone in this challenge; everyone must play a part. Proposals shouldn’t feel like a slog and their authors shouldn’t feel put-upon in chasing them. I’m not the only user who’s followed the forums less closely because of its vitriol.

My feeling on this proposal is a tentative +1. I've followed the proposal since its inception, and strongly feel it is a problem worth solving in Swift.

There are a couple of issues wrapped up in this discussion, and in addressing this proposal the Core Team should seek to improve upon them all.

  • Tooling. The simplest Fix-It in the world is adding an !, and it being incorrect makes it no less attractive.
  • Learnability. preconditionFailure and fatalError just aren't going to come up in a beginner Swift course. I've tried to squeeze them in, and I just get blank stares. It is not an excuse to say "just learn them". They look scary because they are scary. They "crash" and "crashing is bad".
  • Reporting. Some feel that it's hard to locate the source of a failed unwrap, in optimized code but in debug mode too. Particularly for a newbie, a spare ud2 in one of Apple's sanitized crash logs does not always clearly point to the problem.

If the Core Team can provide meaningful improvements w.r.t. the above, then more sigils in the stdlib aren't needed. Either way this proposal should be taken seriously: there are limitations to operator functions that prevent an implementation of !! from being useful in client code, and they should be lifted.

8 Likes

It's a -1 for me. I don't think I have any new arguments to add, but I'll reiterate the reasons already mentioned above that convinced me:

Is the problem being addressed significant enough to warrant a change to Swift?

I believe that it is, but the proposed alternative of making Never a bottom type (something which I believe is worth doing anyway regardless of this specific use case) or providing an overload of ?? with a signature of (lhs: T?, rhs: Never) -> T both seem like better solutions than the introduction of a new operator.

I like the ?? solution because it's what I would expect to work if I didn't know anything about the Never type and its limitations. If I came across let foo = bar ?? fatalError("..."), I'd immediately understand what was going on.

The objection that this solution doesn't work the same way if you use assertionFailure() or preconditionFailure() makes no sense to me because it works exactly the same way as if you used those functions inside a guard statement, which is what this feature is sugar for anyway. If anything it's a ding against the !! operator that it doesn't provide the same granularity of control over how the assertion is handled that fatalError() and preconditionFailure() do.

I think a good way to look at it is: If Never was already a bottom type, and so foo ?? fatalError("") already worked, would there still be a significant incentive to add the !! as well, or would we already consider this a solved problem?

Does this proposal fit well with the feel and direction of Swift?

I don't believe it does. Nowhere else in Swift is an operator used in this way, and I don't think that the proposed !! has the same relationship to ?? that ! does to ? - it's not symmetrical.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

n/a

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I read the full proposal and most of the discussion.

8 Likes

I am on the fence.

I am completely for the ?? Never variant, since I use ?? {fatalError("Message")}() and guard let else {fatalError()} all the time, and I can see the argument for allowing that to be spelled !! Never.

One question: How does it interact with throw?

As for the string variant, I am not really against it, but it does feel a bit too cute, and it doesn't work for things like try!. I want a solution that works in all the places I find a !

I'd like to propose an alternative: Instead of !! "Message", we instead anoint a special comment syntax (I'll nominate //! as a straw man) which Swift sucks up and spits out as the reason for failure of any trap on the relevant line. The advantage to this, is that it uses the behavior already present from the motivation section, and it also works for try!, etc... (basically any trap)

let img = UIImage(named: "Bob")!  //! Couldn't find an image named Bob

Xcode could even color it differently than other comments if it wanted to. I like this solution because the message essentially is a comment... it is just a comment I want to show up in the debugger/console/log when I have trapped.

This would be in addition to ?? Never, which should just naturally fall out of Never eventually...

Thoughts?

@Erica_Sadun, @davedelong, @dabrahams, @xwu, @Joe_Groff

2 Likes

I am a mild -1

For me it is immediately obvious what let foo = bar ?? fatalError("...") would mean while let foo = bar !! "..." is really not that obvious.

There is a string literal that does in no way suggest where/how it is used (hence it is also not obvious what to write there – positive or negative message)

I do find guard statements with fatalError exit actually quite swifty, as they explicitly and unmistakably describe intent. If it is too verbose (say even if you make it a one-liner), then I'd be fine with having the ?? Never syntax.

2 Likes

I'm of many minds about this addition, but on balance: its behavior makes sense given the other operators in the language, I don't think it will cause problems for people learning, using, or extending the language, and I would use it frequently in my own code. Seems great!

My points in favor and against have been trod to varying degrees above, but here's my internal dialogue:

  • It's similar to Perl's or die("message") pattern, but puts the "die" part in the operator instead of the function call. To me, that's good, since it pushes the consequence of the first part failing earlier.

  • There's a little bit of an extra burden to learn what the !! operator does, but it follows so clearly from the rest of the ?/! pairings that it feels like an intuitive extension of/companion to the nil-coalescing operator.

  • It's perhaps a bit too cute that the RHS is a string instead of a function call, but if we're adding more shorthand (since let's face it, the postfix ! is definitely shorthand) let's make it easy to use.

  • Making Never a true bottom type would indeed make this redundant, but it wouldn't make it any less nice to use. The difference between these two statements is the difference between iconography and description:

    let a = optionalValue !! "message"
    let b = optionalValue ?? fatalError("message")
    
  • Many thanks to the proposal authors for this attempt at reckoning with the "Insert !" fixit, and the way it teaches the wrong thing. If this were adopted I believe it would help improve both the teaching and the organic learning processes for Swift.

13 Likes

I am +1 on the problem. New developers do force-unwrap "incorrectly" on a wide scale in a way that is not documented or defended. Xcode's fixits are a major contributor to the issue.

I am more lukewarm on this solution. There are two dimensions to the problem of unexpected nil, of which this proposal addresses only the first:

  1. The cause. Here we are encouraging users to explain why the item cannot be nil. While evidently strange to some people on this list I happen to think this will improve the new user experience quite a bit.
  2. The result. The user must pick from a wide variety of foreign patterns to handle this condition, including
    1. fatalError / force-unwrap. This is a fine pattern as far as it goes but I do think encouraging users to defend them along the lines of this proposal is not a bad idea. Also I don't really buy it, but an argument can be advanced that crashing an iOS application is always The Wrong Thing™, which causes many programmers to avoid this pattern
    2. if-let. Perhaps this is the second-generation Swift developer, who did a lot of IUOs on their first project and has now resolved to no longer write crashing code. But in the case of the unexpected nil it reminds me a lot of On Error Resume Next which is a VB feature every bit as bad as it sounds.
    3. throws. This one behaves sensibly in production, e.g. we can put up a dialog or something and we avoid the Resume Next behavior of an if-let. On the other hand it requires vending a novel type (quite unwieldy in simple situations) and the debugger won't stop in a sensible location. If you know how to configure your breakpoints (new developers do not) you will discover that people throw and catch all sorts of things that aren't your problem and you don't want to be interrupted about in general.
    4. assert. This one is interesting because it recognizes we might want to do something different (e.g. crash) when developing than we do in production. The problem is that what it does in production is nothing, so you have to choose to handle the unexpected nil with one of the other patterns.

While problem 1 is in fact a noble goal, I think the real meat of the unexpected nil learning curve is problem 2. Insofar as this proposal introduces a new syntax for it, the proposal envisions new users should embrace fatalError-like behavior in the typical case. Maybe so, but one could easily imagine they ought to embrace guard-print-return and so we need a short syntax for that.

I agree that we need a better short syntax targeted at new users. I am less convinced about what the short syntax ought to be "long for". I personally suspect it ought to be something that crashes in development but throws in production; arguably something like that would be strictly better in the hackintosh example discussed in the motivation.

In any case, I don't have a real problem with this solution exactly. Unexpectedly found nil while unwrapping an Optional value is an objectively bad error message and any syntax to see less of it is a good syntax. I do think we're punting on the real reason users struggle though, and so that makes this less compelling than I'd like.

+0.5

What if there is more than one trapping-fail on the line? Would there be an alternate "/*! ... */" syntax to intersperse after each fail point?

2 Likes

-1.

Most of examples come from iOS/MacOS development where programmers work with code written in Objective-C, e.g. with UIKit framework when you build table view.

Swift is designed to be nil-safe language, and introducing such operator as part of standard library is opposite to its ideas.

On my opinion, Swift should encourage to do not fail with force unwrap, especially newcomers. Instead, language should encourage to re-write such code: remove optional, provide default value, etc. If no and we 100% percents sure that we want to "unwrap or die" we can use simple guard statement or write operator/function just for our needs.

Another thing is that this code not explicit to anyone. For newcomers it will "just fail you app somehow", for experienced users it will be another thing to keep in mind - what exactly will my program do when I use this operator when with methods like fatalError I can read documentation right inside IDE.

If the main idea to make optionals more clear for newcomers, this is not the solution. It just adds more sugar and magic to the optionals, not an explanation.

No, I haven't.

I read the final proposal and all messages in the topic.

2 Likes

Is the problem being addressed significant enough to warrant a change to Swift?

What I would like to see in the future of the ! unwrap operator is compile-time checking. The compiler would try to verify that any value being force-unwrapped is not nil (using logic similar to how it verifies that all variables are initialized at time of their use).

The compiler would give a warning if it could not directly see that it is okay to unwrap the value. To make the warning go away you would have to either:

(1) Rearrange the logic of your program. For example, place an if-statement around the affected code, or add an appropriate guard statement.

(2) Insert a precondition before the applicable line of code. You would basically be saying, “I know it is not immediately obvious that this value is not nil but please take my word for it, compiler.” Then if the program crashes, it will crash on the precondition, not on the force-unwrap. That’s where it should crash because that’s where the error is — it is in your false assumption that the unwrapped value is not nil.

No more trembling every time you type !. With compile-time checking, you can use ! as freely as any other operator. If you make a logic error, the compiler will tell you.

if a == nil || a! == 0 { return }    // Correct
if a == nil && a! == 0 { return }    // “Cannot verify ‘a’ is not nil.”

This is more the direction I would like to see Swift go in.

1 Like

-1

I've wanted to support this proposal, but, in the end, it simply doesn't seem to fit Swift.

The syntax of Optional<T> !! String is cryptic, and IMO would be a step backwards; a step towards C, and away from clarity of intent. Having an operator stand between, and handle, two different types at the same time would lead to more confusion for new users IMO.

In Swift, we should say what we mean. In that direction lay formulations of the same idea, but which express the user's intent with clarity. I do not champion it as an addition to the Standard Library, but a set of methods on Optional, like the following, would be much more in line with Swift:
.unwrappedButIfNilUse(defaultValue: T)
.unwrappedButIfNilStop(because: String)
.unwrappedButIfNilCall(function: ()->Never)
.unwrappedAndWillNotBeNil(because: String)

A thank you is owed to the sponsors of this proposal. One and all, they are inspiring.

I've studied the proposal, followed the pitch, and have reviewed this thread in detail.

2 Likes

I'm not against having /*! ... */ syntax for completeness, but you could also just split the statement across multiple lines with a //! comment for each (or just decide to have a single comment apply to both traps).

I have already given a detailed response to this proposal here: Resolved: Insert "!" is a bad fixit - #98 by Geordie_J so I’ll try not to repeat myself too much here other than to say I am strongly -1 on this.

To me the actual problem being addressed here is legitimate: that force unwrapping and optionals generally are difficult to understand for a beginner.

The solution proposed here doesn’t seem to be adding anything of value though: every !! “Explanation text” in the proposal has been a case of documenting “what”, rather than “why”. I understand this as a “commenting poor practice” 101. In every case it’s obvious that e.g. “such and such was not a subclass of x”, but how does that help a new user of the codebase at hand, or a new user of Swift?

The issue with force unwrapping is that it should really only be used when you don’t know why the unwrap failed. For everything else you should probably be writing something more explicit (with a guard else fatalError or similar).

The fact that force unwrapping is suggested as a/the fixit is the real issue here. Obviously it shouldn’t be, provided you can give a reasonable explanation why the value may be nil (in which case there are alternatives: ??, and guard else).

Optionals are confusing and we need better diagnostics and more reasonable fixits, but adding yet another operator to the mix is not the best solution to this, IMO it is not even a “good” one.

11 Likes

I want to add a few words about the genesis of this proposal. I have contributed only so much as trying to coordinate a lot of people all of whom are passionate about including !! in the language as an alternative to ??. This proposal includes feedback and direction from more members of the Swift development community with no evolution involvement than any other I've worked on. The people who wanted this feature were using this feature in a variety of forms across many projects.

The proposal grew from developers who wanted sugar for the following:

guard let x = x else {
    fatalError("Reason why x can't be nil")
}

Any focus on learners and new developers is an attempt to appease additional voices during the design phase, which honestly grew completely out of control.

I'm surprised how many people voted in the following fashion: "I am completely -1 on this proposal but I wouldn't mind one of the Never versions." Let me suggest that if you want sugar for this specific construct that you are in fact "+1" for the proposal, even if you don't like the primary design.

A vote against this proposal says: "I do not think Swift should expand its operator set or expand the meaning of the existing ?? operator to include sugar for this construct."

This is, as others have pointed out, and exhausting and overwrought train wreck of a proposal and a proposal review.

I hope as you give consideration to this in the end that you remember, it's just "sugar yes" or "sugar no". The form and design of that sugar can be resolved once that question is answered.

7 Likes

I strongly disagree with this characterization. SE–217 is a specific proposal which delineates a concrete design for an !! operator.

Weighing in for or against SE–217 is exactly and entirely an expression of opinion about the particular design laid out in the proposal. It does not say anything about one’s views regarding other hypothetical future proposals.

17 Likes

I would like to weigh in by reminding that it's not just the +1 or -1 that is needed for a review, but also the gathering of consensus about it; that's why there's a list of questions for people to answer about their motivation. And it may very well be that the proposal is asked to undergo another round of changes, in collaboration with the authors, if we find that the consensus is mixed or uncovers use cases or concrete concerns.

This is a creative and collaborative process between us all. We have different roles and different needs, but I believe it is important to remark that this should be the spirit in which this is taken. I would personally ask that this be kept into account, and not erase neither concern nor, importantly, need, even when it may feel contentious.

12 Likes

I’ll add a strong second to what @millenomi wrote. Swift Evolution would be both a more pleasant forum and a more successful one if we all kept in mind that ultimately the core team decides, and our job is to support their decision-making. That means in part providing a not just a poll but a map of community support for an idea: +1, -1, “yes to the spirit but not this specific design,” etc. It also means providing perspectives, examples, questions, and alternatives the core team may not have considered.

It doesn’t mean winning arguments with each other.

At best, it would mean achieving consensus among all of us reviewers about what options the core team could consider, and what the tradeoffs and implications of those options would be.

At the very least, it should mean a respectful acknowledgement that opinions and arguments will all be heard by the core team, and having stated them clearly and critiqued them thoughtfully is sufficient.

This is something both moderators and participants (myself included) could be better about. When a back-and-forth stops adding new information, it is no longer useful; when it becomes hostile, it is counterproductive. (I appreciated @Joe_Groff’s gentle nipping in the bud such a back-and-forth upthread. I know I’ve been guilty in the past, and I hope the moderator gives me the same respectful nudge next time I am.) I wish these discussions were more creative and collaborative, as Lily said, and less adversarial.

As for this particular proposal, it seems like there’s a lot of interest in several of its goals, and a lot of contention about this particular solution. I trust the core team to hear the many ideas here — even the ones less vehemently stated! — and bring their good judgement to choosing where to take this next. I’m glad that a small group is making that decision, even if I disagree with it, because the alternative is design by mob committee — and that won’t lead to the language any of us want. This is ultimately a cooperative game we’re playing: we all win or lose together as the language we all share evolves.

9 Likes

I have been too busy lately to participate more in the forums, but in light of the last post from @Erica_Sadun and the others following it, here is my evaluation:

Negative. While I applaud the intention behind the proposal, I don't think the proposal is the correct way to deal with the issues raised in its motivation section.

I believe the key issues in this design space that actually need addressing are:

  • How to help developers better understand and correctly use force unwrap
  • How to make the compiler fix-its better and improve runtime diagnostic when the supposedly impossible (force unwrap of nil) happens.

I think we can do better addressing both of the above, without !! operator.

I don't think sugaring if let ... else { fatalError(...) } is a worthy goal to justify a brand new operator.

In contrast, addition of ?? was well worth it, because it is really useful in so many situations. It is now hard to imagine programming without it. On the other hand, if we run into if let ... else { fatalError(...) } frequently enough to justify !! sugar, we better go back and rethink our designs.

Addressing the two issues I mentioned above, yes. Sugaring if let ... else { fatalError(...) }: No.

No, as indicated above.

n/a

Read the proposal and read about 50% of the posts around this issue in multiple threads.

1 Like

Evaluation: This proposal seeks to address a common pitfall when using Optionals in Swift by introducing a syntactic sugar for a best practice: documenting the precondition when force-unwrapping the optional. The benefits of the proposed !! <# Document the force-unwrapping precondition #> syntax include:

  • Encourages thoughtful application of force-unwrapping, compared to mindlessly slapping ! after optional value to make code compile. Replacing most uses of ! use with !! would be, in my estimation, a clear benefit.
  • Additionally it turns that documentation into useful error message, when the precondition fails at runtime. Given this is a programmer error, it’s a bonus that it can also include additional information to help in debugging thanks to the lazily evaluated string interpolation.
  • The assertion documenting string prepended with !! nicely stacks on a new line when composing multiline expressions (e.g. method argument chains), without detracting too much from the main purpose of the code. It fits better than inline comments. This is the mayor usability win over the underlying multiline expression which it sugars.

Perhaps calling this an unwrap precondition operator would be pedagogicaly beneficial over the “unwrap or die operator” (it doesn’t play well in the context of official documentation).

Concrete implementation details should be left for later code review (!! being alias for ?? -> Never makes perfect sense; including correct line numbers in error message is a must; preconditionFailure should be the correct sugared function invocation pending SR-905 fix). Making Swift Evolution Proposal Review into 500 person code review is counterproductive waste of time that fosters unnecessarily hostile atmosphere.

Problem significance: As demonstrated with StackOverflow question popularity above, the “insert !” fixit is making this problem as prevalent as NullPointer Exceptions in Java. So even though Swift avoids the billion dollar mistake by disallowing null references and has a type-safe remedy in form of Optional, we are not reaping its full benefits in practice.

Wider programming perspective: From what I’ve seen in practical use of Haskell’s Maybe and Scala’s Option, they don’t suffer from the issue. The problem this proposal seeks to address is unique to Swift because of our use of force-unwrapped operator.

Swift fit: Proposal logically extends the family of Swift’s optional handling operators with new signpost: !! clearly indicates dangerous operation and is easy to teach, in the spirit of Swift’s progressive disclosure for new learners. It offer’s a better alternative to !, which should be reserved as absolute power tool: to be used only when it’s crystal clear it cannot trap. Then we can address the root cause of the problem: replace compiler’s ! fixit with ?? and !! (in that order).

8 Likes

One last comment about how this operator came to exist in my codebase, and why I believe it deserves inclusion into the standard library.

When I'm writing code, I generally want Swift to stay out of my way. Dealing with sharp corners in Swift is disruptive to the creative process.

This operator stemmed from the observation that it's common to want to unwrap optionals, but that the existing patterns all are either too verbose or fail the "is this expressive enough" test.

if let

if let foo = maybeFoo {
    // use foo here
}

This is the standard approach, but if you know ahead of time that maybeFoo can be known to have a value, this introduces unnecessary indentation and cyclomatic complexity.

Postfix-!

let foo = maybeFoo!

This approach definitely wins on terseness, but it fails on expressivity. I have no explanation for why maybeFoo is allowed to be force-unwrapped like this.

Postfix-! with a comment

let foo = maybeFoo! // an invariant condition has been violated

This is marginally better than a bare force-unwrap, except that it'd be nice to have that explanation in the logs if this ever fails. That makes tracking down problems easier, and gives me a leg up on triaging crashes.

guard let

guard let foo = maybeFoo else { fatalError("an invariant condition has been violated") }

This is a better approach than a bare force-unwrap, and for the occasional optional, this may be fine.

But once you start dealing with lots of IBOutlets (my style is to never use implicitly unwrapped optionals, for the same reason that I never use postfix-!), it gets fairly tedious.

So it marginally passes the expressivity test, but fails finding a balance with terseness.

?? fatalError()

let foo = maybeFoo ?? fatalError("an invariant condition has been violated")

This definitely terser, but using ?? (which never crashes in any other context) feels like the wrong approach.

!! fatalError()

let foo = maybeFoo !! fatalError("an invariant condition has been violated")

Using ! over ? in the operator is semantically more appropriate, but you again fail the terseness requirement (and the desire to Make Swift Get Out Of Your Way™) if you need to use this more than about 3 times. Repeating fatalError("this is why") everywhere gets pretty old pretty fast.

Thus...

Infix-!!

let foo = maybeFoo !! "an invariant condition has been violated"

This is a great balance between expressivity, terseness, debugging aids, and readability. The operator would use fatalError and not preconditionAssertion, because the latter gets compiled out in certain builds, which defeats the entire "debugging aid" goal.

It's nice to also have !! () -> Never, if you come across a situation where you want to do additional logging/capturing/whatever as you crash, so that you can supply your own wrapper to fatalError().

4 Likes

@davedelong would you or any other proposal authors take a few second to comment on the reasoning behind the choice of !! operator instead of ?! like I mentioned above already twice. The left ! would actually mean force-unwrapping the lhs parameter while it really isn't. I'd really appreciate an answer to that question.

As I noted above !! is just symmetrical to ?? while above you explain what ? and ! means in a different way. In that situation I don't think we should push symmetry over actual semantics of the operator.