SE-0217 - The "Unwrap or Die" operator

An expansion of ?? to accept a Never on the right-hand-side will not work. On the surface it looks appealing:

let value = someOptionalValue ?? preconditionFailure("Must be non-nil")

However, this falls apart in various compilation modes when someOptionalValue is nil:

  • -Onone: correctly emits the error message
  • -O: crashes, but without an error message
  • -Ounchecked: does not crash
  • -Osize: crashes, but without an error message

One of the major points of this proposal is to get clearer diagnostics when things do crash, which means you need reliability that it will crash, and if it crashes, you need to guarantee that the crash reason will be there. Allowing ?? to accept a Never on the right hand side does not meet this criteria, because there are certain Never-returning functions that the compiler will just straight-up eliminate.

Only fatalError() meets the requirement for the combination of "always crash and always produce the diagnostic message". However, since there isn't a way to indicate that ?? would only want fatalError() on the right-hand size, it is not a feasible course of action.

7 Likes

This sounds to me like we need a more general way to panic() since fatalError and friends won’t cut it.

fatalError() is the only "crashing" function that will always execute and always emit its error message, regardless of compilation mode.

2 Likes

Do we have information on the number of significant Swift projects which have defined their own “unwrap or die” operator? How about coding standards that recommend its use?

This has been discussed on Swift Evolution for long enough to be well known, meaning that if prominent developers / project leads found it beneficial then they would have already implemented and instituted it “in the wild”.

Since “unwrap or die” is simply syntactic sugar which can already be created by anyone who wants it, I would conclude that if it has not gained traction and entered common use on its own then it should not be added to the standard library.

4 Likes

This seems like a bug. I don't believe fatalError is supposed to optimize out like that.

It doesn't; I was testing with preconditionFailure to illustrate the inconsistency in accepting any Never-returning function.

1 Like

Yeah, but that's a deliberate design choice of preconditionFailure and assertionFailure. This problem pops up anywhere you use them, and has nothing to do with whether using ?? with Never is a good idea or not.

7 Likes

The optimization-mode behavior of preconditionFailure is irrelevant. This is still valid code, regardless of which -O flag you compile with:

guard let value = optionalValue else {
  preconditionFailure("optionalValue was nil")
}

Since preconditionFailure returns Never according to the type system, it is permitted to be the final statement in a guard block, and as such it would also be acceptable on the right hand side of a hypothetical ?? operator that takes a Never.

The documentation for preconditionFailure says:

Meaning that it would be programmer error to place it on the right hand side of a ?? whose left-hand side was nil, just as it is programmer error today to place it in a guard block that fails.

6 Likes

Yes, it's valid code. However, the point of the proposal is to improve diagnostics and code expressivity, and allowing preconditionFailure in this manner means you'd be missing out on the "improved diagnostics" goal.

2 Likes

If someone chooses to build with -Ounchecked, they are making the conscious decision to lose such diagnostics if they use preconditionFailure, because that's the documented behavior of that function. They are saying "this will never execute, I guarantee it, and if it does, I screwed up." If they want the diagnostic, they should use fatalError, or some other logging function of their own choosing that guarantees termination.

You can't hold preconditionFailure to a different standard if it's in an hypothetical ?? expression or in a guard statement.

7 Likes

-1 on the !! syntax.

+1 on this syntax.

4 Likes

+1 but with modifications. I would suggest:

  1. ?? Never overload be added, as suggested by others in this discussion.
  2. !! “...” is a shortcut for ?? fatalError(“...”).

Yes, the use of ! should be discouraged and the proposed fixit is a big improvement.

I think it reads very naturally and will be easy to explain.

No

Followed the original thread, followed this thread, and read the proposal.

1 Like

I’m very much a +1. I find it unfortunate that there seems to be a strong opinion by many that force unwrapping should never be used, because I often use force unwrappings as assertions of program state to give me assurance I won’t accidentally mask bugs. With this proposal, the comments I normally write above these force unwrappings will appear in any crash logs, which is incredible useful.

As an aside, I do not think using myOptional ?? fatalError() is a good alternative. I consider ?? to be a “safe” operator to ensure the program won’t crash, and pairing it with fatalError would make the code harder to understand at first glance (especially when assigning an optional to another variable....that just seems incredibly strange).

4 Likes

This statement would hold if ?? only accepted literals but we are allowed to call a function in which case all bets are off on how safe that func will be.


var some:String? = nil
func `safeFunc`<T>() -> T {
    fatalError("oops")
}

let boom = some ?? safeFunc()



1 Like

Preferring ?? to !!

If you read the proposal, you'll see that we discuss this as an option and explain that

The new operator is consciously based on !, the unsafe forced unwrap operator, and not on ??, the safe fallback nil-coalescing operator. Its symbology therefore follows ! and not ?.

Several people have brought up the option of using a () -> Never rhs with ??, the nil-coalescing operator. This is highly problematic when using a rhs that is compiled away (assert, for example) in release builds.

Even if the team prefers to use a () -> Never rhs, I'd still recommend using !! over ?? to indicate the imbalance in roles between the safely unwrapped lhs and the unsafely unwrapped rhs. Expanding Never to be a bottom type does not fix this symbology mismatch. Whether Never is or is not a bottom type is orthogonal to this proposal.

Writing the operator yourself in your own projects.

As in SE-0199, the counter to "write it yourself" is that Swift deserves nice things. This is a community-sourced evolution proposal, which exists in many dev environments. This operator:

  • Commonality Applies across many problem domains and is a fundamental part of calling any nullable method
  • Readability Enhances readability and produces run-time diagnostics in a way that comments cannot.
  • Consistency Offers a consistent approach that encourages best-practice explanations of why an optional can never be nil.
  • Correctness Moves unwrapping from use-sites to declaration-sites.

The core team wrote,

The core team would welcome further proposals to fill in gaps like this in the standard library.

Our goal is to increase expressivity and enhance diagnostics. This operator is a nice thing. Swift developers deserve nice things.

Function vs Operator

It feels natural to fit !! into the same use-space as ?? but provide for the situation where there can never be a fallback and the unwrap should never fail.

If the issue is "String" vs "Function", then !! can be followed by () -> Never adding a bit more verbosity (and the same issue of assert/etc being compiled away), but the spelling should be unsafe ! and not safe ?.

5 Likes

In previous phases of this proposal, I've favored the ?? + bottom Never alternative, because it gives us several different constructs like this. However, my thinking has evolved and I now support this proposal.

Instead of writing a lot of boring words about why this proposal is a good idea, I've prepared a visual aid:

Basically, I think that !! guides people into better use of force-unwraps. Merely asking people to "explain why this cannot be nil" may help people learn when a force-unwrap is appropriate and when they should do something else instead.

And that teaching advantage is one that ?? fatalError() can't match. This is why my opinion on !! changed: Even if we supported Never values on the right side of ??—and I still think we should!—the !! operator would still be a good idea purely as a shorthand for a best practice we very strongly recommend.

I have a couple of minor concerns:

  • I'm worried that infix !! may look confusingly similar to infix ||. One way to address this would be to change the operator to a single !; that would still be confusable with the | operator, but that one is used much less often. The parallel to ?? may be more important, though.

  • The inability to point to the location of the error correctly is very disappointing. It's something we can try to improve later, though—and I can think of at least two ways to do it besides the proposal's suggestion, one much deeper and the other much hackier.

But despite these small issues, I think this proposal is a great idea and, if it's accepted, I expect to use it pretty often.

9 Likes

Either spelling seems fine to me, i.e. I want a () -> Never rhs.

The issue of assert etc. getting optimised away is the same for any conditional execution of assert etc. and is therefore more a problem that assert etc. have than with ?? (or however it’s spelt).

1 Like

For the record, assertionFailure does not have a return type of Never so putting it on the rhs of ?? won't even compile.

6 Likes

Some people really prefer not to use force unwrap regardlessly and never ever go with 1 or 4 below, therefore go with 2 or 5. But they aren't better at all because 2 is too redundant for nothing and 5 isn't as clear as 4. For them, 4 and 6 work great because it's as concise as 1 and 4 while you don't have to use force unwrap.

I think we shouldn't encourage people to "hate" force unwrap but at the same time, I feel force unwrap is maybe like "goto"; it should probably better be discouraged to use until you really understand when to properly use it.

In that sense, I understand this proposal can be beneficial.

let url = URL(string: "https://example.com")!  // 1
guard let url = URL(string: "https://example.com") else { fatalError("can't happen") }  // 2
let url = URL(string: "https://example.com") !! "valid url"  // 3

guard error == nil else { throw error! }  // 4
if let e = error { throw e }  // 5
guard error == nil else { throw error !! "valid error" }  // 6

I'm generally +1 on this proposal. Here's my two cents:

A common pattern in interop code with legacy C API looks like the following.

// C API creating a status object.
let status = NewStatus()
// This C API returns a nullable pointer, but expects users to 
// check `status` first.
let value = someCAPI(status)
checkStatusAndDieOnFailure(status)
// Now, although `status` should guarantee that `value` is 
// non-null, we want to check it so that we don't have internal
// inconsistency errors.
guard let unwrappedValue = value else {
    fatalError("Status was success but the pointer is null!")
}
// Then use the non-null pointer.
return unwrappedValue

In this example, since value is a local variable, one can't write guard let value = value to shadow the optional value. Some pragmatists (though not me) would prefer to write the following because they feel that coming up with a new variable name is hard and cumbersome.

guard value != nil else { 
    fatalError("Status was success but the pointer is null!")
}
return value!

The !! operator, or even the ??+Never alternative, would define this problem away.

let value = someCAPI(status)
checkStatusAndDieOnFailure(status)
return value !! "Status was success but the pointer is null!"

My concern with the ??+Never is that ?? strongly indicates "unwrapped or default value". Even if we make Never the bottom type, fatalError() is not a default value. So I would prefer sticking with the exclamation mark.

I understand that making !! take an @autoclosure () -> Never could generalize the API a bit and allow custom fatal error crashers, it's arguably a bit more complex for most cases. I'm indifferent, but we could have both at some point.

1 Like