Deprecate ! and make Never the bottom type

Could we deprecate ! and make Never the bottom type?

Old:

let offset = letters.firstIndex(of: fragment)! // deprecation warning + quick fix

New:

let offset = letters.firstIndex(of: fragment) ?? preconditionFailure()

Reason:

!s are hard to spot in complex real-world code.

2 Likes

@owenv created a post a while ago to discuss making Never a bottom type.

In the meantime, you can get the same effect if you do something like this:

func ??<T>(lhs: @autoclosure () -> T?, rhs: @autoclosure () -> Never) -> T {
	guard let _lhs = lhs() else {
		rhs()
	}

	return _lhs
}

let offset = "ABC".firstIndex(of: "D") ?? preconditionFailure() // failure

or just rewrite it as guard let offset = letters.firstIndex(of: fragment) else { preconditionFailure() } which is pretty close to the original.

7 Likes

Efforts to make force-unwrapping more visible by making it more verbose faired poorly in review and are unlikely to make it into the langauge. I like the idea personally, but it looks like it will forever be an idea that lives in user extensions. (Here's the self-documenting one I use, and how it looks in context.)

However, making Never a true bottom type does always seem to get support whenever it comes up. I see @suyashsrijan already linked to the recent discussion. I'd love to see the questions in that thread resolved. (Also, nice trick making ?? handle rhs Never like that.)

8 Likes

My post about Never linked above is a little out-of-date, but I have a WIP PR on making Never a bottom type here. I haven't had much time to work on it in the past few weeks but I'm hoping to get back to it soon. There are unfortunately some complications around source + ABI compatibility, but I think they can all be resolved.

5 Likes

Personally, while I usually discourage its use, I wouldn't want for ! to go away. It's a useful tool in some situations (e.g. especially in scripts, which is one of the goals for Swift 6 I believe).

I do encourage a progression of Never to become a real bottom type though, but I didn't see anyone really against it

14 Likes

First, as many others here would point out, this is source-breaking and thus is very unlikely to be adopted.

That said, I think this is part of a wider class of issues where Swift isnā€™t quite as safe as I would like for certain cases. Iā€™d really love an annotation we could use for such cases, in this case something like ā€œno fatal errorsā€, which could apply to either the function, type, file, or target scope. This would make it very clear that there are no force unwrappings in that code. (Note that even this is nuanced because currently things like Int.+ crashes on overflow, and we may want to automatically use a variant which throws an error instead)

Another thing like this I would like is ā€œno unsafeā€, maybe both of these are actually part of a single concept like ā€œsafe modeā€.

Note that crashing is one way in which safety is guaranteed. When an invariant is violated, stopping execution is safe, while continuing is unsafe. This is the sense in which Swift uses the term "safe."

17 Likes

This is fantastic, thank you for sharing! Added to my toolbox.

Swiftlint will help you exterminate these from your code, if that's what you want.

2 Likes

Nice formulation. This seems like a no-brainer add to the stdlib to me. :man_shrugging: (Not mutually exclusive with a proper bottom type, just a bridge between here and there.)

2 Likes

Addition to the stdlib are never "no-brainer". You have to think about what it will prevent you to add/change in the future without breaking ABI.

Will we be able to remove it once we have a real bottom type, or will we have to carry it forever ?

2 Likes

That's a pretty good one, at least when Optionals are involved :slight_smile:

On a slightly related note, I use a similar thing in my projects where we have TODO<T> or undefined<T>, defined like:

public func TODO<T>(
    _ hint: StaticString, 
    function: StaticString = #function, 
    file: String = #file, 
    line: UInt = #line
) -> T

which sadly bumps into an annoyance with type inference.

This one specifically when Void is to be inferred in a function (and no explicit return), though really what I'd be after is Never being an actual bottom type so it could be used in such ways: func f() { TODO() } as well as let x: SomeX = TODO() would be able to work more nicely, with just one definition. So yeah, would be nice to have Never be an actual bottom type, though no idea why it isn't today :thinking:

For reference if someone else cares about this: Type inference (of Void) for generic method fails without explicit return [SR-12488] Type inference (of Void) for generic method fails without explicit return Ā· Issue #54930 Ā· apple/swift Ā· GitHub

1 Like

May as well add type: T.Type = T.self, might help with Void too.

1 Like

Huh, interesting -- that does indeed help with the Void case, a bit weird :thinking:

Good idea, but this seemed to weird to be true -- and it was :slight_smile: (still had the previous definition in the REPL). Sadly this yields the same result:

  1> func TODO<T>(t: T.Type = T.self) -> T { fatalError() }
  2> func x() { TODO() }
error: repl.swift:2:12: error: generic parameter 'T' could not be inferred
func x() { TODO() }
           ^

I put it pretty much anywhere in generics like that. IMO it's a poor API if generic resolution relies solely on an output type.

Yeah, though sadly I've amended my reply above -- does not really make a difference.

Sure, but it's fairly common and useful to defined "todo" and "undefined"'s in laguanges which have a bottom type (e.g. we'd use tons of def foo: String = ??? in Scala (where ??? returns Nothing (the bottom type)) to make placeholders while WIPing a shape of an API. One could argue it's not useful for much else other than those "placeholder" functions but yeah, it's quite nice to have.

no I mean, that there's no fallback should the type checker failed (like in this case). At least you can do:

TODO(t: Void.self)

Though arguably, you can also use as

TODO() as Void

in which case, you can remove t: from the signature entirely (though I'd suggest that you keep it).

Out of curiosity, why are you declaring lhs as an autoclosure ?

So you can do something like ā€ABCā€.first ?? preconditionFailure()

Isn't it still fine to use non-closure on lhs?