Moving toward deprecating force unwrap from Swift?

In our codebase we ban force unwrapping using SwiftLint which I'm quite happy about, our crash rate is extremely low and it forces us to think about edge cases. That said using force unwrap is required in some places, like when we hardcode URLs in our config files, and there we can disable the swift lint rule on that line.

Also not every piece of Swift code needs to be production ready. Scripting in Swift is a thing and being able to force unwrap to exit early if something is unexpected is really useful, and allows writing scripts quickly and concisely.

If the Swift compiler allowed disabling warnings then deprecating force unwrap would be acceptable, but unfortunately that seems like it will never happen. Adding warnings to people's codebases without the ability to silence them really isn't useful as it adds noise that will hide other warnings in the codebase.

For example recently a warning was added for "Immutable property will not be decoded because it is declared with an initial value which cannot be overwritten" we knew this and the behaviour that happens is expected but now our project has a 100 warnings getting in the way of real ones because there's no way to say actually this is the behaviour we want (without changing the perfectly valid code)

This should be left up to linters until we can disable compiler warnings.

1 Like

What I am missing is a safe unwrap operator for such cases:

func f(value: Int?) {
    if value != nil {
        print(value¡) // ok, safe to unwrap
    }
    print(value¡) // compile time error, unsafe to unwrap
}

I took ¡ for this example, the best would have been to choose ! for safe and !! for unsafe unwrap. But that ship has sailed unfortunately...

What I am missing is a safe unwrap operator for such cases:

That's what the if let … expression is for – otherwise you could only run into scenario I've just described. There's no easy nonnull guarantee with simple if sth != nil condition.

1 Like

It cannot be emphasized enough that ! is the safe unwrapping operator, because trapping is safe. The spelling for unsafe unwrapping is unsafelyUnwrapped.

9 Likes

I am aware of that! I guess I should have written „safe to unwrap without trapping“ for you

For this, you should have a "non-empty collection" :)

Property assessment reports.

final class ReportsDatabase: ObservableObject, Diskable {
    
    @Published var fetchedItems: [Report] = []
    
    var property: Property!
    
    var file: File {
        try! property.directory.createFileIfNeeded(withName: "reports.data")
    }
}

The reports correspond to a specific property, so until the user selected the property, I wouldn't know which reports to load.

The 'Diskable' protocol offered a function called 'fetch', which would in turn populate the array.

If this crashed because property was nil then surely every human being armed with a keyboard would know what was going on?

It's extremely useful for scripting and prototyping. SwiftLint is for warnings and coding conventions.

4 Likes

That works only in a throwing context, and suggests that it can throw forcing the person reading that code to waste brainpower.

Using a non-empty collection there would be nice, but we don't have thatin the standard library :( Writing my own would be a super complicated solution to choosing a random string.

I don't think that programmers using a language feature recklessly, is a good enough reason to remove it. The force-unwrap operator is absolutely transparent in what it does, and programmers know what to expect when force-unwrapping a nil value. It is the programmer's responsibility to use it mindfully.

Furthermore, I believe that banishing force-unwrap with linters is the ideal way of doing it, because the rules on the linter can be customized by the team that owns the codebase, hence the linter works as an enforcer of that team's agreement on how they envision their codebase's style. Different teams can agree on different rules, and that's ok. That kind of decision don't feel like belonging in a language-level.

Lastly, force-unwrapping provides you shortcuts to get to the point faster, if you are working on a project that is not meant to be "on production" and you don't really care if it crashes.

6 Likes

Use the right tool for the right job:

if let error = error {
  print(error)
  return
}

And we already talk about the Kotlin smart cast and while it is fine for simple cases, there is far too many cases where it can't work to make it really useful.

If Kotlin had support for a if let equivalent, it would remove the need for almost all smart casts, and will work in all cases, not only when the compiler is smart enough to perform the cast, making Kotlin code far more consistent.

One (among others) big limitation of smart cast for instance is that it does not work across module boundaries, so moving a type from on module to an other may break the code using this type, making refactoring harder than it should.

1 Like

The nice thing about guard is that you can tell by just looking at the condition - without looking into the else branch - that you leave the scope here. This is not the case for if and if let. If you are one of those developers who want to communicate intent in that more global way, you'd have to embed the rest of the function into an else. If you need to do this multiple times and you're unlucky enough that you need to execute some code in between those checks so that else if is not an option, you end up with deeply nested elses which is unreadable.

Unfortunately, support for structured programming in Swift is not much better than in most other languages. I'd love to have annotations for control-flow statements (if, if let, switch, for in, while and while let) to express "this thing always leaves scope", "this thing sometimes leaves scope" and "this thing never leaves scope", but in order to not be source-breaking, they'd have to be opt-in (which means either a lot of noisy warnings in old code bases or no enforcement of the annotations at all) or implicitly added and shown to the user (which would be a paradigm shift for Swift).

John Sundell had a similar idea

That would be great but making the Swift compiler even slower than it is today would be nobody's favorite thing :frowning:

What could be done instead in a more swifty way would be for example: introduce a bunch of built-in protocols that would help create non-optional overloaded variants of functions like randomElement() or URL(string:). Say there are built-in protocols NonEmpty and ValidURLString with automatic conformance for literals.

Though the price of making those functions non-optional seems a bit too high but who knows maybe people will find some more creative ways of using those protocols in other situations.

1 Like

Time to wrap this pitch up. Apologies for the provocative OP, I’ve moderated my position since then. Along the way I’ve concluded:

  • Deprecating the force unwrap postfix operator is out of the question, source breaking changes to Swift at this point are out of scope. Many are still confident they are able to wield it with the discretion it requires though I’m not completely convinced. There is however a use case in less critical code bases such as for scripting.

  • Things might have been slightly better if Swift had adopted Kotlin's !! instead of ! as it would have been easier to audit for.

  • It may be useful to create non-nilable initialisers for URL when it is initialised with a static string as this was so often the case-in-point.

  • I have continued to explore these alternatives in the Unwrap SPM package

  • I submitted the PR in the end to add a -warn-force-error option to the compiler though It was rejected pretty quickly as “the role of a linter”.

It even blew up on Twitter where you can gauge exactly what the broader community feels:

It is still my belief it's regrettable that force-unwrap is one of the first things developers new to Swift learn and likely one of the last to master. Having the vision to build nil-able types into a language and then negating much of the benefit with such a flimsy construct has pros and cons but in the end I came down of the side of “it does more harm than good”. If it's an assertion, require an annotation, add a bit of friction so it receives the attention it merits.

Replacing it with a guard is not quite the answer either. When debugging, nothing is worse than a section of code that fails silently so remember to log something to the console. I assume this is the origin of the belief that “safe” crashing is preferable but this doesn’t pan out well in the hands of users.

Thanks for the replies, All the best for 2021.

7 Likes

Is adding

Optional<U>.forceUnwrap(
  _ message: @autoclosure () -> String = String(),
  file: StaticString = #file,
  line: UInt = #line
) -> U

to the standard library out of the question?

I feel users, particularly novice users, are unlikely to actually use a Swift package that adds such a method. The key is to allow unwrapping with a custom failure message and as little additional syntax as possible. Something you could teach before mentioning !.

I feel the rationale for putting this in the standard library itself is similar to that of Result: there are numerous third-party implementations doing the same thing, it is generally useful across pretty much every Swift program, and it improves readability at the point of use.

Hello John.

Although you proposed something that you knew would be provocative among the community, and I happen to disagree with you on this topic, that twitter thread you linked has somewhat saddened me. I have never interacted with you before but I’ve seen you around the forums since the earliest days and know you are not just someone looking for trouble or looking to stir up the community unnecessarily, so I read your proposal seriously without reading too much between the lines, while keeping an open mind. I feel some of the feedback in that twitter thread has the power to demotivate other people from wanting to share their opinion and experiences on the language in the future, in the forums. I know you had to be prepared for it even before writing the proposal, hopefully it didn’t affect you too much and you will continue writing new proposals in the future.

All the best for 2021!

Ricardo

10 Likes

It is an interesting solution, but why do you think compile time optimisation is not Swifty way? As I remember, Swift already does some computation while building, such as checking that a dictionary is not initialised with duplicate keys or calculating simple expressions like [1, 2].map { $0 * 2 }.sum(). I see your point about compilation time, but if reducing build time is a critical moment, we should probably disable these features too.

What I mean is, the Swift compiler is already slow. I have examples of trivial CGFloat expressions with some 6-7 operations that take literally seconds to compile. At least on macOS, don't know if it's the same on Linux.

I'm not familiar with swiftc internals but optimizations and compile time evaluations like the above could be done on the llvm level in principle. constexpr on the other hand requires a higher-level evaluation, because the compiler needs to know whether a given expression is const or not in ther first place, i.e. it's not a question of optimization. That's my understanding anyway. C++ does a lot of this type of work when expanding templates etc. but I suspect for Swift it will be a lot of additional work that would slow it down even more. I might be wrong, though!

1 Like

You are right. Although I really like the idea to allow developers do some work at compile time, I absolutely agree with you, it will make the compiler even slower :pensive: