SE-0217 - The "Unwrap or Die" operator

Oof, that's pretty unfortunate. But I really don't see a good reason to treat !! differently from any other precondition—we ought to care about source location leaks and code size vs. error descriptiveness exactly as much for !! as for any other precondition.

You say it. We just can't use preconditions in production code.

Sure. But... I won't use !! if the string does not pop up in crash reports. And I will tell people to avoid it as well. I'd say that fixing SR-905 is mandatory for !! to be based on preconditionFailure. On the other side, this proposal can start with fatalError, and move to preconditionFailure afterwards. Anything is OK as long as !! plays its score: REPORT errors (regardless of code size).

3 Likes

I would if it could be “chained”.

?? allows

let result:String = some ?? some2 ?? some3 ?? ""

would this work?

let result:String = some ?? some2 ?? some3 !! fatalError("oops")
3 Likes

I would change my vote from opposed to neutral with the optional !! fatalError("error") syntax.

It would also remove the need for Optional ?? Never, about which I am strongly opposed.

1 Like

Completeness is far better served by making Never behave properly, which would make x ?? fatalError("message") (and a bunch of other things) “just work.”

7 Likes
Character Description Postfix Infix
? The "safely execute" character ? - execute this expression if it's not nil. If it is nil, no-op. ?? - execute the left if it's not nil, but if it is nil, execute the right
! The "unsafely execute" character ! - execute this expression if it's not nil. If it is nil, crash. !! - execute the left if it's not nil, but if it is nil crash because of the right

Why not "?? () -> Never"

It would be incongruous with all existing precedence to make anything relating to a ? allow for crashing. ? is always a non-crashing operation and was created explicitly to avoid crashing.

On the other hand, ! always indicates the potential for crashing when it comes to optionals. The unary version, like unary ?, doesn't allow for fallbacks. Therefore, the binary version, like the binary ??, would crash because of whatever's on the right-hand side.

This is what I mean by "completeness". Using ?? to crash is incongruous with all other uses of ? in Swift. !! is the only logical spelling if we want to have a "crashing binary operator".

6 Likes

You are holding it wrong (in your head) ;-).

There is no crashing inherent in allowing x ?? fatalError("message") other than what is explicitly allowed by the execution of fatalError. This is just the same as the currently-legal code:

func f(_ x: Int?) -> Int {
     return x ?? (fatalError("message"), 42).1
}

In which it is the execution of fatalError() that causes the trap.

I should be clear about what I am saying the right answer is: Never should be a subtype of (implicitly convertible to) every other type; that is the only change I'm proposing. Failing that, the fallback position is to write your own overload of ?? as follows

func ??<T>(lhs: T?, doesNotReturn: @autoclosure()->Never) -> T {
    while true {
        if let x = lhs { return x }
        doesNotReturn()
    } // this loop never repeats
}
7 Likes

There is absolutely crashing inherent in the creation of:

public func ??<T>(lhs: T?, rhs: @autoclosure () → Never) → T

It's right there in the presence of Never.

I’m a +1.

The strength of the resistance to it puzzles me a bit. This is a low-burden proposal, both in language surface and implementation. It’s good for both (1) code readability and (2) runtime diagnostics in the wild; either would be sufficient to justify it. The utility-to-burden ratio is high.

Examples of usage in practice

Concrete examples are helpful. Here are a few from actual code.

Here the code needs a non-nil URL that is valid, but will always generate errors if anyone attempts to load it:

url = URL(string: ":")!

It is possible that this dubious assumption about : passing syntactic muster as a URL might change with a future version of Foundation. If it does, this once-working code will suddenly fail in production with a bollixing crash — hopefully traceable back to the offending line, but we all know how tenuous that can be in practice.

Imagine, however, that we have the !! idiom to follow:

url = URL(string: ":") !! "A single colon is a valid URL for which all requests fail"

Now the underlying assumption is documented in the code, and the runtime error we get if Foundation shifts underneath our feet is much more useful: “Fatal error … blah blah … Assumption failed: A single colon is a valid URL for which all requests fail.”

The !! operator improves both readability and debuggability.

(There’s a debate to be had about whether the message should be phrased as the positive assumption whose failure is an error, or as a description of the theoretically impossible error. I prefer the former, as above, because it reads better as documentation.)

Here’s another example, where the assumption underlying the force unwrap is less localized and thus less obvious:

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)

In this second example, in my actual code, I have the same message in a comment:

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

Making this information (1) part of a standard idiom and (2) surfacing it at runtime both seem preferable to the ad hoc comment.

Versus more generalized proposals

I like the idea of Never being a true bottom type; however, that’s a large and complex question, and there’s no reason to hold up this simple proposal to support in it. In the future, the proposed !! operator could just be sugar for it; this proposal does not harm a hypothetical bottom type.

However, I do not like the idea of ?? fatalError("…") instead of !!. Why? For the same reason I like have the ! operator: concise, idiomatic clarity. Code using these operators assumes it is impossible for a particular unwrapping to fail (or at least that a failure is reason enough to terminate the whole process). This is a fatal error, and there is no need for spelling out a recovery path; the only question is whether the underlying assumption is worth documenting in each particular instance. Spelling out ?? fatalError("Assumption failed: …") is just information-obscuring boilerplate.

Note how the more verbose syntax harms both readability and ergonomics in my examples above:

url = URL(string: ":") ?? fatalError("Assumption failed: A single colon is a valid URL for which all requests fail")

// vs

url = URL(string: ":") !! "A single colon is a valid URL for which all requests fail"

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)

Yes, it’s more explicit. No, it’s not more readable. This extra syntax is enough burden to harm adoption in practice; presented with this option or !, developers will be much more likely to choose the bare, message-less force-unwrap.

The concise !! syntax, on the other hand, presents no more burden than a // comment followed by the same message. It draws attention immediately to the information-bearing text.


Despite the long argument above, I don’t have fiery feelings about this proposal. But it seems sensible and useful. Like I said, the utility to burden ratio is high — higher than many extant features.

5 Likes

Sorry, but that's wrong. A never-returning function need not crash; it just has to not return.

func f() -> Never { while true {} } // totally legal.

My example demonstrates that there is just as much crashing inherent in our current support for

x ?? (fatalError("message"), 0).1
4 Likes

Fair enough... but in practical app development, the number of times you ever write your own Never-returning function like that is basically zero. If you need a long-running thing, we have lots of other better-and-more-appropriate constructs to use over running an infinite while loop.

In practice, the only Never-returning functions we'll ever use are dispatchMain() and the crashing functions. (And even dispatchMain() is exceptionally rare)

And what if you were instead?

I wonder if putting emphasis on Never is sensible, considering a proposal which explicitly targets "both experienced and new Swift users", but with an big emphasis on new users.

I know we are all more or less experts here, and that we daily deal with nevers. But for the rest of Swift users, there is a big leap from from (value ?? "default") to (value ?? fatalError("...")). This leap is Never.

As a matter of fact, even experts keep on learning about Never:

Sorry, but that's wrong. A never-returning function need not crash; it just has to not return.

Yeah, you're right, but this is not the topic. I wish the "newbie facet" of the proposal were given more consideration, and its purpose in general. Give help.

3 Likes

I'm all for giving help. What I'm not for is spending an operator and standard library surface area on a use-case that I consider of dubious value in general and can be expressed with more general means whose implementation would benefit the language overall. Once written, the ?? fatalError() idiom is unmistakably clear and readable. I consider !! to be less readable and not significantly more discoverable. There's no great win here, and adding stuff to the language/standard library should meet a higher bar, IMO.

27 Likes

I've been a touch hesitant to support !! String, but I would strongly support !! Never.

let x = value !! fatalError("Something bad happened!") is less terse, but it opens the door to better logging / customization by the consumer.

I also couldn't agree more with Dave's take on avoiding ?? Never.

If ! is the unsafe companion of ?, then !! should be the unsafe companion of ??.

3 Likes

While I agree, to play devil's advocate here: new users could legitimately misinterpret the idiom to mean that some sort of error is thrown and would only halt execution if it isn't caught elsewhere (given that it uses the word "error" and ?? might look like the inverse operation of try?).

1 Like

Whether to rename fatalError is a separate discussion (Error was introduced after fatalError, FWIW).

4 Likes

For you, IMO.

On the other side, the mental model described by @davedelong here holds straight just as well, and is quite sensible. The (? / !) pair as (don't crash / crash) has much more precedent than ?? Never: try? / try!, as? / as!, etc? / etc!


Edit: besides, if !! is sugar for ?? fatalError(...), everything is there right in place! You'll use ?? fatalError(...), maybe enjoy a little sip of !! sometimes, and everybody will be happy.

3 Likes

See also:

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!