Deprecate ! and make Never the bottom type

Yes

I'm using the following approach to avoid force-unwrapping, but Paul's is tidier, and I plan to use it.

guard let url = URL(string: "https://www.codinghorror.com") else {
  fatalError("🙀 url could not be initialized.")
}
2 Likes

Thanks, use away!

The thing I like about mine is that the grammar of the comment is structured to work as both source documentation and error diagnosis. In the source, it reads like a comment explaining why the developer reasons a force unwrap is OK:

paramString.data(using: String.Encoding.ascii)
  .forceUnwrapped(because: "URL-escaped strings are always ASCII-representable")

…but if you get a crash, it reads like an error message:

Fatal error: Unexpectedly found nil unwrapping an Optional.
Failed assumption: URL-escaped strings are always ASCII-
representable: file foo.swift, line 30
4 Likes

It's an idiom. Of course, we the Swift community would never introduce a three-line method without talking about it to death.

What harm would there be in doing so? Even if there were many variations of Never-as-not-quite-bottom helpers in the standard library, the implementations would quite frequently be inlined into and subsequently optimized out of compiled code. Heck, as a trivial syntax sugar, many such helpers wouldn't necessarily need to be committed to the ABI at all. Either way, we're talking on the order of bytes. Several thousand bytes more were processed for me to load this page and make this post.

1 Like

From the rejection announcement of SE-0217:

The review discussion raised the possibility of adding an overload ??<T>(_: T?, _: Never) -> T as a stopgap, but in experimenting with this solution, we found that overloading had unacceptable impact on type-checking performance, and adding the overload presents a non-zero risk that, if making Never be a universal subtype does not end up happening for whatever reason, then the overload on ?? would be left as a strange special case in the standard library.

I don't think we should consider introducing new stdlib API surface until we've had a chance to fully evaluate the performance impact of the more general solution. It's possible making Never a true bottom type will negatively impact type checker performance, but I'm fairly confident introducing new stdlib operator overloads would be worse. Introducing new overloads now might also make the more general solution impractical later.

3 Likes

I think a lot of force-unwrapping could be avoided with better awareness of safe unwrapping techniques. In particular, Optional.map(_:) and Optional.flatMap(:_) are criminally underused.

In the end, though, there are often cases where crashing is the best option. For instance, I deliberately force-unwrap stringly-typed Core Data code during model-loading to ensure that mistakes are caught immediately. The ! operator makes that much easier to read and write.

2 Likes

I personally get a little jolt whenever I see that ! anywhere in my code. There are plenty of cases, like @Saklad5's above, where their context and purpose are obvious to me at the first couple of glances. However, I will be implementing @Paul_Cantrell's forceUnwrapped(because:) method almost everywhere else. It actually has a nice ring to it, and adds delicious intrinsic documentation and debugging to my normal flow.

1 Like

A minor nit on implementation though, @Paul_Cantrell. If you add #file and #line to forceUnwrapped(because:), then the error message will point out the spot where forceUnwrapped is called, rather than where fatalError is called internally. Is the latter behavior intentional?

For example, in my testing at least, the error message points you to the file where forceUnwrapped(because:) itself is defined. If instead, it were defined this way:

public func forceUnwrapped(because assumption: String, file: StaticString = #file, line: UInt = #line) -> Wrapped {
    guard let self = self
        else { fatalError("...", file: file, line: line) } // Note the context arguments passed to `fatalError`
    return self
}

then the fatal error message points you to the callsite.

3 Likes

Yes, a nice improvement!

You should get a little jolt when you force-unwrap.

One of my pet peeves, by the way, is things that are Optional despite having no possibility of being nil. In particular, CodingUserInfoKey.init(rawValue:) is awful. It literally can’t be nil, you can see that in the source code.

1 Like

I prefer !. It's short and simple. It contrasts well to ?. Dropping it and using ?? will make lots of ternary conditions difficult to read.

1 Like

The failable initialiser specifically for CodingUserInfoKey is considered a bug and can probably be ameliorated by the addition of an unlabelled not-failable initialiser.

That's why I think it was a mistake to use ! for both a dangerous operation like force unwrapping and for a harmless operation like negation.

At least Kotlin uses !! for unwrapping, so it's easier to detect and search for.

2 Likes

While I prefer code that immediately traps when lost in the state space, such behavior should be always clearly and explicitly stated with words such as precondition() or preconditionFailure(). Using hard-to-spot, hard-to-search-for, easy-to-sprinkle, multi-purpose interpunction for explicit trapping is "actively harmful” in my book. Clarity over brevity.

3 Likes

That makes sense. Perhaps one way to help the hard-to-search-for part in the short-term may be to expose the force-unwrap operator as a function, so at least Xcode can list its callers for us to find. Perhaps even adding a compilation option to warn or error when it is used without good reason.

Are we including the implicitly-unwrapped optional syntax here, too? I personally have found use for them whenever a variable has no definite initial value until a certain complex condition is satisfied in the present function. As long as I'm careful to define the variable before I use it, and declare the variable appropriately close to its conditional initialization, I see no problem with it.

@Paul_Cantrell I’d change the name of Optional.forceUnwrapped(because:file:line:) to Optional.safelyUnwrapped(_:file:line:). That mirrors the syntax of both Optional.unsafelyUnwrapped and XCTUnwrap(_:file:line:).

Force-unwrapping is safe, in terms of avoiding undefined behavior.

1 Like

Most things are safe in swift by default, so it doesn't make sense to include the word "safely". For example there is no Array.safelySorted method. Additionally, removing the word "force" would make it confusing to know in spoken conversation which way of unwrapping you meant, because there are lots of ways to safely unwrap

If the variable is defined inside a function, why would you need to declare it as implicitly unwrapped? I find that mostly necessary for properties.

If the variable is defined inside a function, why would you need to declare it as implicitly unwrapped? I find that mostly necessary for properties.

Heh, actually, I had one case in particular in mind when I wrote that, as I was sprinkling forceUnwrapped(because:) into one of my helper packages. Looking at it again, it actually makes sense to declare it as non-nil variable, and let the compiler yell at me if I use it before I initialize it! No implicit unwrapping needed there. :sweat_smile:

I too use it for properties all the time. It's nice when I declare something like a Combine observer that acts on self from an initializer. My AnyCancellable should never be nil, but it cannot have a value until self is initialized. Since I initialize that property in init, implicit-unwrap makes sense here.