SE-0217 - The "Unwrap or Die" operator

As I have demonstrated several times now, it is a misunderstanding that supporting x ?? fatalError() violates that precedent in any way. Making the change proposed would only validate and reinforce that misunderstanding. I'm strongly against making changes to the language or library whose justification is based on misunderstanding.

7 Likes

OK you win. Good night folks!

And as I explained, that position is pedantically correct but practically wrong.

But if there’s no room for flexibility in understanding here, then I’d rather see this proposal withdrawn than implemented in what I’d consider to be a fundamentally bad way.

Like @gwendal.roue, I’m bowing out of this discussion. I don’t have the mental or emotional energy to keep fighting this level of pedantry.

2 Likes

The goal of a review discussion shouldn't be to "win" but to gather information relevant to evaluating the proposal. @dabrahams, I think you've stated your case, and likewise, @davedelong and @gwendal.roue's objections are equally clear. To keep the discussion productive, I propose we move on.

12 Likes

I wasn't going to add a "me too," but since we've obviously reached a point where it wasn't crystal clear to everybody else on the list: me too.

1 Like

One last thought while on my iPhone:

@cal makes a great point about !! () -> Never providing a customization point.

I would be happy to see that in an accepted solution as long as there was also the shorter !! String version.

And if the “Namespacing common error scenarios” proposal is accepted, I would also expect a !! FatalReason to be part of that.

3 Likes

The use case for this feature is basically "I want to force-unwrap a value, but I am a mere mortal who is sometimes confused about my preconditions". In other words, the use case for this feature is basically "I want to force-unwrap a value".

But let's focus on one specific sub-case: users who don't really understand optionals yet. I find this Stack Overflow question instructive on the question of whether developers are currently understanding force-unwraps. This error message only comes up when you either force-unwrap incorrectly, or when you access an IUO when you shouldn't, and a developer who understands these features should not need to look up the message. Thus, traffic on this question may be a good proxy for confusion about force-unwrapping and/or IUOs.

As I write this, the question has 216,821 views, 281 upvotes (meaning users thought the question itself was a good one), and 134 favorites (meaning users bookmarked it for later reference). The top answer has 497 upvotes and has been opened to community editing so it can serve as an authoritative source. These are all huge numbers, akin to, for instance, the question about EXC_BAD_ACCESS in Objective-C. Taken together, these numbers suggest to me that users frequently force-unwrap inappropriately and struggle to understand what went wrong.

Moreover, they continue to struggle. I went back and looked at this question in the Wayback Machine so I could chart its activity over time:

Unwrap%20stack%20overflow

It doesn't look like people are getting much less confused about when to force unwrap.

To me, the crucial part of this proposal is that it modifies the fix-it on optional misuse, replacing the "Insert '!'" fix-it with a pair which insert either ?? or !!. I think this would help users who don't understand optionals yet in three ways:

  1. It would offer an option besides force unwrapping, so users would be aware that there were other ways to fix the issue.
  2. It would replace the force-unwrap fix-it with one which requires more thought to use (you have to fill in the placeholder for the right-hand side), so you're less tempted to "just make it work" by choosing that option.
  3. The placeholder's wording implies that you should only use !! when you're certain the value can't be nil, so some users may figure out that it's not the construct they need.

Without !!, we could still change the fix-it to suggest ?? or ! as alternatives. But inserting ! would still be the easier fix, and there would still be nothing to make it obvious that this might not be the right thing to do.

Maybe this isn't the right solution. But it's hard for me to see this use-case as having "dubious value" when 5,700 people have struggled with the status quo in the last three weeks.

(P.S. It looks like @jtbandes has been moderating this Stack Overflow question and making sure it remains helpful for confused Swift users. Thank you!)

9 Likes

I certainly agree that what you highlight is a problem in the learning and teaching of Swift. However, I see the proposal as setting us back rather than moving us forward in this respect:

Consider how long that (very nice!) answer is on StackOverflow. I suspect that providing yet another option--essentially, another flavor of force unwrapping--is only going to help new users become more confused about the feature. And I'd imagine that it would be rather a setback for those intermediate users who have already heard the advice "don't force unwrap," now to be told that there's a brand new one (can I use this one because it's different from the other one? or should I also avoid it because it's a force unwrap?).

8 Likes

As I'm trying to form an opinion and write some form of review, I read the replies and find myself constantly swayed on the various sides. It seems to me there are good reasons to go in every direction. Here's my train of thoughts:

  • I like the idea of !! "explanation" because it encourages the use of meaningful error messages. But if it does not support telling you which file and line the error comes from, then it's a regression from plain ! in my opinion.

  • I like the idea of having !! fatalError("message") as !! makes it clearer that we're stopping as opposed to ?? fatalError("message"). But the later ought to be allowed anyway if Never becomes a bottom type. Also, forcing Never does not really guaranty the program will stop (someone could throw an exception, run an infinite loop, etc.). So for these two reasons !! fatalError("message") seems problematic to me.

  • I think !! doesn't work very well with optional chaining:

    var result = someObject!.run()
    // vs.
    var result = (someObject !! "did forget to load your nib file?").run()
    

    This could make a fixit suggestion unwelcome if it becomes the default.

  • It's hard to gauge how much introducing a new unwrap syntax would help beginners. I do agree with the argument that it might have the opposite effect and make things more confusing since now you have two syntaxes for roughly the same thing.

  • If beginners have difficulty understanding what's happening with failed unwraps, I think the the solution should be to improve the error message instead inventing a new syntax. For instance:

    fatal error: force-unwrap of optional value with '!' failed: 'myArray.first' is nil

    I think that would be more effective than asking users to write an explanation to fill the placeholder of a fix-it.

I think in the end I'm unconvinced that !! "message" is worth including.

11 Likes

Here is the thing where I personally disagree with your post. For me the postfix ? is "execute the expression if possible or fallback to a no-op for the further chain", while the postfix ! is "execute the expression with the assumption it's not nil, if the assumption is wrong than the crash is your fault". In that sense !! does not make sense at all since it signals that it will do a force-unwrap on the lhs rather then unwrapping it safely before crashing on rhs. That said the unwrap or die operator would look like this ?! which I already mentioned in the post #49.

3 Likes

-1

If users don't understand optionals let's make them more understandable: a well-informed workaround is still a workaround.

A think I already stated my concerns at the time: I personally consider force-unwrapping always harmful, but a necessary evil in some cases (when dealing with XIBs, for example), and I never use ! in my code if not for things like URL initialization with a string (and even there, I try to cook up a type that represents an URL in a type-safe way). I also never use preconditions, which are very popular in dynamic languages: Swift type system is powerful enough to always avoid them, or almost always. At the time, my position encountered a lot of resistance, that I didn't expect, because from the very beginning I consider the strict safety of Swift as a clear advantage, that should be worked around only in extreme, exceptional cases. Somehow many people didn't feel the same, and I honestly still don't understand the arguments in favor of !.

I think a path forward for Swift should enforce the basic design principles ("Opting for safety sometimes means Swift will feel strict, but we believe that clarity saves time in the long run") instead of relaxing them. But I understand the argument that !! will make the user more concerned with their force unwrapping, resulting in (possibly) more thought put into the matter: but to really have this effect, we should consequently remove the ! postfix, as! and try!, that would be substituted, respectively, by !! _, _ as? _ !! _ and try? _ !! _. This way users would always be forced to better document their forced unwrapping, resulting in a net positive.

2 Likes

One of the most important aspects of this proposal is its value as developer documentation, as lightweight literate programming. There’s been a lot of discussion of syntactic and type system questions (which are interesting & important), but let’s not lose sight of the effect on code readability.

The heart of the proposal is to capture a valuable piece of information: the thinking underlying a force unwrap. That piece of information will be seen far more often in the code than at runtime; in fact, it is precisely developers seeing and understanding it in the code that will prevent it from being seen at runtime!

I’d thus like to draw attention a little more to how different approaches work as information design in the code. In this example code from upthread, there is an assumption that the compiler cannot enforce, but the developer has helpfully chosen to communicate. What is that assumption? How easy is it for another developer to spot it and understand it in each of the following syntaxes?

return request(method,
    data: urlEncodedParams.data(using: String.Encoding.ascii)! // A URL-escaped string is already ASCII
    contentType: "application/x-www-form-urlencoded",
    requestMutation: requestMutation)

return request(method,
    data: urlEncodedParams.data(using: String.Encoding.ascii) !! "A URL-escaped string is already ASCII",
    contentType: "application/x-www-form-urlencoded",
    requestMutation: requestMutation)

return request(method,
    data: urlEncodedParams.data(using: String.Encoding.ascii) ?? fatalError("Assumption failed: A URL-escaped string is already ASCII"),
    contentType: "application/x-www-form-urlencoded",
    requestMutation: requestMutation)

The last has the advantage that it makes the logic and control flow more explicit, but has the disadvantage that it moves the documentation 31 characters farther away from where it is relevant. I’d argue that the documentation, not the control flow, is the crucial information here.

Maybe one of these hypothetical namespaced errors could split the difference here by moving some of the contextualizing info (“assumption failed”) from the message text to the process-halting function’s name? Just making something up:

return request(method,
    data: urlEncodedParams.data(using: String.Encoding.ascii) ?? Fatal.assumption("A URL-escaped string is already ASCII"),
    contentType: "application/x-www-form-urlencoded",
    requestMutation: requestMutation)

That’s not ideal either; I’m just throwing it out there for consideration.

I’m not deeply wedded to the specific syntax of !! "explanation", but would be disappointed if the ultimate solution represented a readability regression from // explanation.

7 Likes

+1

I write the guard with fatalError more than I might like to admit. It is the correct strategy in many situations.

I've read the proposal and the discussion.

1 Like

The standard library is meant to support the language's built-in and near-built-in features. So it should approach "simple as it can be but not simpler," to paraphrase (disputed) Einstein. This borderline feature is supposed to be part of a fix-it, so it could be categorized to be part of the standard library.

I don't think I understand why you keep comparing, for example

!! "A URL-escaped string is already ASCII"
?? fatalError("Assumption failed: A URL-escaped string is already ASCII")
// instead of
?? fatalError("A URL-escaped string is already ASCII")

i.e. why is the “Assumption failed:” there? The design in the proposal seems to just pass the message straight to fatalError, so it feels like you're trying to unfairly penalise the alternative.

I feel fairly neutral about this proposal, so I haven't commented so far. The ?? fatalError alternative seems fine to me, but perhaps it's beneficial to have a shorthand for that. I do think that if it is accepted then the “calling context” feature from future directions should be required at the same time, because having the error message point to the wrong place will be a very confusing experience, particularly for the beginner users that the proposal is partially targeted at.

1 Like

The proposed operator knows that the fatal error is due to an invalid assumption causing a force unwrap to fail. I am assuming — not unreasonably — that the !! implementation’s error message would provide that context, producing output in this general spirit:

Fatal error: Unexpectedly found nil while unwrapping an Optional value.
Underlying cause is a failed assumption: A URL-escaped string is already ASCII. file Foo.swift, line 128

Simply calling fatalError without the additional context, as you suggest, provides this magnificently misleading output:

Fatal error: A URL-escaped string is already ASCII. file Foo.swift, line 128

This makes it sound like the error is that the string was ASCII, when in fact the error was that it wasn’t. One could reword the message to product the correct output, but look what that does in the code:

urlEncodedParams.data(using: String.Encoding.ascii)
    ?? fatalError("A URL-escaped string was not ASCII")

That’s not the end of the world, but it has a “double negative” quality that harms readability: “this should never not be true”. Remember that:

  • the message will be read more often in the code than in the console, and
  • its purpose is to document an assumption that should always be true.

Phrasing the assumption in the positive is helpful in the context of the code. It’s reasonable to assume that !! (or whatever sugar there is for this case) would provide contextualizing output to allow that phrasing.

If not at the same time, then soon on its heels. Agreed: line numbers matter!

2 Likes

I currently think of ??, and the hypothetical !! that it inspired, as a form of “or” so I'm not sure that the positively-phrased version is more readable and I would expect that the negatively-phrased version (e.g. “A URL-escaped string was not ASCII”) would be used. On the other hand, some people (including you) might be thinking of it as representing what they might write in a comment beside a forced unwrap, so !! would be read as //. I don't see that one interpretation is clearly more natural than the other, which is actually serving to convince me that the shorthand might be confusing enough to not be valuable.

Yes I certainly see this proposal as (2) surfacing in the console what is still primarily a comment, and also, even before that, (1) encouraging people to actually write the comment in the first place. I’m in favor of something that accomplishes these goals. I’m open to suggestions on alternative syntaxes that serve them better.

That form is certainly awkward. But wouldn't the appropriate rewrite be:

var result = someObject?.run() !! "Did you forget to load your nib file?"
1 Like

Not appropriate for the general case. For one thing, it doesn't work in the situation where your chaining contains more than one unwrap, especially if one of them is not a force-unwrap:

var result = someObject!.parent?.run()

For another, run() could itself be returning an optional and you'd be force-unwrapping the result of run().

Beside the subtle change in semantics on what situations can make the unwrap fail, it also moves the message further away from the cause of the problem you want to detect. I find this makes the code less readable.