SE-0217 - The "Unwrap or Die" operator


(Chris Lattner) #162

Personally to me, this seems like the most important motivation of the proposal. The writing of the proposal itself gets bogged down with the fact that Xcode is still producing the x! fixit instead of x ?? <#default#> fixit, and I found that to be distracting and beside the point.

If we were motivated to solve the case above, I would ask this set of questions (to be clear, none of the names suggested below are serious proposals):

  1. How common is this case to be worth sugar?
  2. How prominent and common are "good" uses of x! (in contrast to the bad ones like Xcode is pushing people towards)? I know that NSURL and other APIs make x! the right thing, but are these common enough to warrant a change?
  3. If we are going to sugar the above (very verbose!) code, why go all the way to an operator? Why not go with a method on optional?
  4. If the above is important enough to sugar, then why aren't all the other trapping operations, like try!, as! and array subscripts? If our goal was to improve those, then a library feature that installs a thread local failure string would be a better solution IMO, that would allow: let x = failureMessage("reason why x can't be null") { x! }. You could even use an autoclosure to get rid of the punctuation soup there.

To elaborate on point 3 above, the entire point of the x! syntax is to be concise and to support chaining of expressions like x!.foo(). Including an error string defeats the rationale for having a terse operator: the proposed !! operator isn't really useful except at the top level expression. Chaining with it would require parens, and the logic would be pushed to the right of the string. Given that, something like this: let x = x.forceUnwrap("reason here") seems to achieve the same goal, allows reasonable chaining, is more clear, and is still less verbose than the thing you're trying to fix above.

To reiterate my position on the original post, the proposal as written seems odd in that it is a partial solution to sugar an operator that many style guides say not to use - and those style guides don't generally say "write guard + fatal error" AFAIK, they say to use x? or ?? or other existing mechanisms. IMO, the introduction of new operators should have a very very high bar, because they directly add "language complexity" that every Swift programmer will be expected to memorize. If this were important to address, it seems that adding a method would be a better way to go for clarity of code, and to avoid putting additional language weight into something we don't really want to encourage use of.

-Chris


(Dave DeLong) #163

I can't speak for the others, but in my code, I use ?! to indicate something that is "more dangerous than ??, but more recoverable than !!"

They go (in terms of recoverability) from:

?? → no recoverability issues. no crashing. no errors. Use the left if possible, otherwise use the right.

?! → some recoverability issues. no crashing, but throws an error defined by the right if the left is nil:

public func ?!<T>(value: T?, error: @autoclosure () -> Error) throws -> T {
    if let value = value { return value }
    throw error()
}

Usage example:

public extension JSON {
    
    public func value<T: JSONInitializable>(for key: String, path: String? = nil) throws -> T {
        let o = try self.object ?! JSONError.wrongKind(self, expected: .object)
        ... use o as a strongly typed JSON object value
    }
}

!! → never recoverable. crashes. uses the right to explain why the left failed.

public func !!<T>(value: T?, error: @autoclosure () -> String) -> T {
    if let value = value { return value }
    fatalError(error())
}

Basically "the more ! it has, the more dangerous it is". ?! is therefore more "dangerous" than ??, but less dangerous than !!. To me, that indicates that ?! would throw an error (which is "more dangerous" than nil coalescing, but less dangerous than crashing).


(Adrian Zubarev) #164

Okay this is your personal preference, but what would speak against two overloads on ?! which can throw or die. I mean this whole proposal is about 'unwrap' lhs (?) or die on rhs (!). Would be interesting to see the opinions of the other proposal authors in comparison.


(Dave DeLong) #165

That's definitely an option, but personally I would be opposed to having a single operator spelling do two different things depending on the type of the RHS. I think that would violate my goal for a high degree of expressivity in code, because I'd have to stop and introspect types in order to know what's going to happen.


(Tim Vermeulen) #166

This seems to be the cause of a big part of the discussion in this thread. IMO it's not ?? that crashes here, it's fatalError. To me this is like saying that

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

is bad because guard isn't "supposed" to crash.


(Dave DeLong) #167

I can see that line of thinking ("?? is sugar for an if ... else statement"), but I disagree with it, mainly because ?? is an operator, and guard... is not.

The comparison fails because of Postfix-!, which is also sugar for an if ... else statement, except it crashes:

let foo = maybeFoo!

is equivalent to:

let foo: Foo
if let f = maybeFoo {
    foo = f
} else {
    fatalError()
}

Yet no one seems to have a problem with Postfix-! causing a crash. Why is !! different?


(Adrian Zubarev) #168

But we alread doing that with ?? where one overload returns the rhs optional or the other overload a non-optional rhs. I think ?! would be the natural fit if something like this would be accepted with this proposal.


(Dave DeLong) #169

:point_up:️This is an important point. If we proposal authors had been notified prior to review, we would have revised this, and the name would likely have been changed to the "Unwrap Because" operator, because technically Postfix-! is already "unwrap or die".


(Dave DeLong) #170

But isn't that mainly so we can support chaining?

let foo = maybeFoo ?? maybeOtherFoo ?? maybeThirdFoo ?? defaultFoo

Yeah we can have it to pick one optional over another, but that (intuitively) seems far less useful to me than wanting to end up with a non-optional.

:man_shrugging: my style disagrees, for reasons I've articulated multiple times in this thread. (!! is analogous to ??, !! is "more dangerous" than ?!, etc)

I guess we'll have to see what the Core Team says.


(Pavol Vaskovic) #171

I don’t understand why you seem to ignore the reality of prevalent misuse of the ! operator. The argument about it’s use for chaining falls squarely into the “power tool use”, which no one’s questioning. The reality of everyday Swift is that this single operator is responsible for complete negation of type safety benefit brought on by the Optional type in practice. It is the smoking foot-gun of Swift, see the evidence from StackOverflow. When this issue was raised in the Resolved: Insert "!" is a bad fixit thread, @rudkx asked what should be done instead, to not let users hanging with code that doesn’t compile. This proposal offers a concrete solution that is in line with rest of DSL for Optional handling in Swift which is operator based.


(Erica Sadun) #172

Fair point. Yes, it's redundant and heavy but it provides an important deployment fix.

I want a solution. Sugar seemed to be the easiest solution. At this point, I'm more wedded to a commitment to solving this issue than to sugar.

Yes, these are common enough to warrant a change. They are especially rampant in Cocoa Foundation but every failable initializer is a potential use-case.

I know the other authors (both named and unnamed) care a lot about the design. I'm motivated by the problem. I want a fix that is in keeping with the spirit and direction of the language and I don't want this proposal to be rejected and then the problem never brought up again because it's been debated and discarded.

That said, I have used the operator. I like the operator. For reasons stated upthread, it mimics ?? coalescing (and no, while I prefer !!, I would not throw a fit with ??), it is ergonomic, it is transformative in that it turns a code-level comment into a black-swan-level usable diagnostic.

Further, it ties into the work we've been doing on creating FatalReason. Individual types with failable initializers could extend prebuilt errors, allowing the rhs to be .unconstructableURL or .missingImageAsset, with autocompletion support.

This is focused on solving one real world problem rather than a full class of theoretical ones. It offers a narrowly focused utility with limited impact and measurable benefit.

The !! operator is so naturally related to ?? that it has a significantly lower cost to both recall and recognition than a completely novel operator. Just because new operators are usually the wrong answer does not mean they are always the wrong answer.

And even though I think Optional<Wrapped> ?? () -> Never is a slightly inferior design, I'd much rather have that than allow the language walk away from this common problem space.


(Dave DeLong) #173

Erica addressed your other points, but I'd like to think about this momentarily on a thread tangent.

I actually think that all operators should just be sugar for underlying method calls. I like this from a symmetry and completeness point-of-view, and I also like it because it would establish a baseline for what would be required in order to add a new operator (it has to be sugar for a commonly-used method).

This thread isn't the place to discuss this, but on the whole I would also love to see:

public extension Optional {
    public func unwrap(_ because: @autoclosure (() → String)? = nil, file: StaticString = #file, line: Uint = #line) → Wrapped {
        if let value = self { return value }
        let message = because?() ?? "Unexpected found nil value when unwrapping value of type \(Wrapped.self)"
        fatalError(message, file: file, line: line)
    }
}

(Paul Cantrell) #174

Same here.

I very much like the spirit of this idea. I particularly like that it could cover several failures within a single expression.

I’d want the actual syntax & naming to favor readability a bit more: less noise, minimized boilerplate, clarity about how the message should be phrased, etc.

This is an excellent question, and I suspect the answer varies quite a bit between different programming domains. If we are going to do another design round on this problem, as seems likely, I'd want to assemble a nice set of concrete, real-world examples taken from a variety of kinds of project (iOS, server side, system tool, C interop, etc.) before finalizing and reviewing the design.


(Jacob Williams) #175

I see a lot of people mentioning any sort of Cocoa interaction as a valid and frequent reason for force unwraps, but I'd also like to point out that using C APIs can also lead to potential force unwraps.

In a path library I'm working on, I try to make things as "safe" as possible before calling into C APIs, some of which may return NULL pointers. In several instances, I can eliminate any possible reason for the C APIs to return a NULL pointer (ie: the only C API requirement is for the path to be less than PATH_MAX), and so I use either a force unwrap or a guard let x = x else { fatalError("...") }.

There are plenty of APIs across C, Foundation, Cocoa, etc that return optionals, but that developers can relatively safely presume will not return nil and if they do return nil then a fatalError is the only appropriate solution.

I would like to see a solution to this that is less verbose than guard let x = x else { fatalError("...") }.

I'm not a big fan of:

x !! "Something went wrong"

because I'd like the ability to do more than just use fatalError. Sometimes it may be a recoverable error and so I'd just like to throw without having to write out a whole guard ... else ... block.

x !! fatalError("Something went wrong")

Is definitely more brief than the whole guard let x = x..., but still clear enough that anyone (beginner or veteran) should know what is going on.



As someone who is definitely not a Swift "expert", a ? with any kind of optional signifies a "safe" operation (AKA won't crash) and the nil-coalescing operator is taught on several beginner tutorials as "unwrap the optional or use the default value provided."

The ability to throw/crash to me just seems contrary to the intent of the ? which is why I am opposed to ?? using a Never return type. Doing so would require learning what Never is and why that means it's appropriate to use fatalError instead of the default value you were originally taught should go on the right-hand side of the ??.

On the other hand, ! is taught from the start as an "unsafe" operation (AKA may-crash). And so the !! operator makes logical sense to me.

Now once you get into some more advanced Swift topics you start to realize that these "beginner assumptions" aren't exactly true and that there is a lot more to the ! or ? than you originally thought, but as I read through this thread from the beginning it was brought up several times about how this proposal is meant to help beginners and how it shouldn't be confusing to beginners etc.

In my opinion, the ?? spelling with Never as the return type definitely makes this confusing for beginners who may only know that ! means unsafe and ? means safe. They may come across someone else's code and see that the supposedly "safe" ?? operation looks really dangerous with a fatalError on the rhs. It seems like a really bad idea to shatter their perceptions of ?-related operations in something as common as the nil-coalescing operator.



I actually really like @davedelong's idea:

I'm a huge fan of keeping ?? as "safe" as it currently is and introducing the ability to either throw or crash with ?! and !! respectively. If we had these 3 options, then I wouldn't be opposed to using just a string for the rhs of !!.


(Jacob Williams) #176

Another issue I have with force unwrapping is the poor error message/debugging on Linux (where I do most of my dev).

In my path library when I run swift test on Linux I occasionally get a "forced unwrap unexpectedly yielded nil" (or whatever the generic failed force-unwrap message is). There is no line number or anything so I can't tell which force unwrap is failing and I haven't figured out how to use lldb with swift test so I'll generally just clone the repo onto my Mac, run swift package generate-xcodeproj and run the tests in Xcode where it will tell me exactly where the force unwrap fails.

For the most part, I only use forced unwraps where it should be 100% safe and then in places where I'm less certain I use guard + fatalError.

Unnecessary Information

Unfortunately for me, the force unwrap that's failing seems to occur in a section of code that shouldn't even be executed during the test, so I've got more debugging to do. My test opens a lot of directories very quickly, and it seems like I'm running out of open file descriptors for the process and that's having unexplainable side-effects.

Another issue is that expressions aren't permitted at the top-level (where the failed unwrap is happening), so I'm rather stuck with my force unwrap (but this is a problem for another day) instead of a guard + fatalError.


(Braden Scothern) #177

I am -1 on the proposed design. And -0 on the issue being addressed.

I have been lurking for a good while and read the pitch when it came about, the previous review posts, and heavily discussed the topic with co-workers. Through all of that I feel like the current proposed solution is a band-aid on what is (and needs to be) a sharp and powerful edge of the language.

I understand that optionals can be difficult for new Swift developers but having looked over the latests release of the swift programming language I feel like optionals are clearly described. I feel like fixing the fixit to suggest the use of ?? defaultValue will be enough to help alleviate this problem while helping users of Swift become more knowledgable.

It feels like the proposed solution is simply trying to force developers to write a comment that ideally shouldn't be needed in most cases. I would argue that knowing how and when to use ! to force unwrap is an important skill to have so you can fully understand the execution flow and architecture of the code you are working with. It should be clear from context that this should be a safe operation or else there is often bigger issues in the code you are working with and a crash is appropriate. When things aren't simple enough that a ! gives the context to understand the "safety" of unwrapping, a guard let with a fatalError() doesn't seem like a lot of boilerplate at all.


(Matt Rips) #178

The intent of the proposal is to make it easier to explain, and in turn to understand, why we are using the force unwrap operator. The spirit of the proposal is both to save us from ourselves and to make our intentions clear to others, including our future selves.

The Two Use Cases of the ! Operator

This discussion has been hampered by a conflation of the types of reason underlying the two use cases for the ! operator. The proposal contemplates a function that takes a String, with that String being a “reason why”. However, the two use cases of the ! operator have fundamentally different types of reasons, and packing those two different types into a single function is confusing. In other words, our discussion is suffering from unsafe type usage of two different subclasses of the English-language reason class.

To illustrate, consider a pair of pseudo force unwrap methods on Optional, with exaggerated signatures:

The Favored Force Unwrap Use Case:
.unwrapAssured(thisValueWillNotBeBilBecause: “reason stated here”)

This use case is undertaken with deliberation and reason as to why nil cannot occur. One should take an extra moment to document that reason.

The Disfavored Force Unwrap Use Case:
.unwrapUncertain(ifItIsNilThenItIsBestToCrashBecause: “reason stated here”)

This use case is a product of haste or expediency, with the acceptance that a crash very well may occur. Here, the reason is not about why the crash will occur, but instead about why the user is willing to allow a crash to occur (e.g., this is a prototype, and we will build the sad path later; there is no good default value, and I don’t want to write error handling code for this edge case).

While the latter use of the ! operator is disfavored, it very well may be the dominant use case. While a ?? fix-it will reduce usage of the ! operator, the user often will prefer to implement the disfavored use case of the ! operator.

The Language Can Teach the User

The language is able to teach the user. Requiring the user to explicitly select a use case would clarify intent, and teach the user about the difference. Requiring the user to supply an explicit reason as to why the disfavored use case is being implemented would help the user learn to want better patterns that avoid the dead end.

Making a disfavored use case into an official part of the language is not desirable. Yet, that approach may be superior to the reality of millions of users shoving square pegs into round holes on a daily basis.

If there is to be a Standard Library solution, perhaps that solution should teach programmers to distinguish between the two use cases, and also prompt them to learn the bad reasons underlying their use of the disfavored case.


(Paul Ossenbruggen) #179

I like the ?! as well, I would prefer to use that than !! in my usage. You still need to put try out front so it does not eliminate the other variants where you just want it to die as fast as possible. The big plus is getting it to a single line, and making safe code easier to write. The ! makes it too easy to write unsafe crashing code. ?! makes it almost as easy as writing ! (this also seems similar to what I wrote above, although even more well thought out).

Think of this usage pattern, developer is just getting something working, so they don't deal with all the formalities of throwing on a nil or handling the error cases. I have seen thing quite often in the code base I work on with many developers of varying abilities. They are in a hurry. They never go back to fix these issues until it is in shipping code and we start getting crashes. It is easy to miss because ! is easily lost in all the other code inside expressions. If we can ban ! in most of our code base then we can eliminate one of the biggest causes of crashes.

I think we need to make doing the right thing as easy, or easier than it is now, doing the wrong thing should be harder. This seems to be in line with making Swift a safe language. Maybe we should not focus on !! but making it easier to not do a force unwrap.


(Chris Lattner) #180

+1 from me. This is what I am arguing for on the thread about improving operator type checking.

-Chris


(Chris Lattner) #181

+1. I would also particularly like to see a version of the motivation that focuses on the core aspects of the language, not a bug in a fixit.

-Chris