Moving toward deprecating force unwrap from Swift?

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:

core team feedback

Hi John,

Thanks for the thread wrap up. The core team discussed this thread and, as you surmise, deprecating the force unwrap postfix operator is out of the question, though not just for source stability reasons. We believe force-unwrap and force-try are valuable tools and would not entertain a proposal to deprecate or remove them. We think adding some wording to this effect to the Commonly Proposed document probably makes sense.

Participants are welcome to continue to discuss the stylistic merits of avoiding force unwraps, but that should be done in the Using Swift section of the forum rather than the Evolution section.

Cheers,
Ben

17 Likes

Hi Ben, thanks for the feedback. I'm still happy I asked the question. I've had a difficult relationship with force-unwrap over the years from the very beginning. Could we not have had an opt-in warning as a compromise? The PR was prepared and is of very modest scope. Go on, Go on Go on Go on.

1 Like

What's the motivation for an opt-in warning as opposed to the status quo, where users who wish to discourage/prohibit force unwrapping are free to use a linter to produce a warning/error?

Well that's all very well but my last encounter with a linter was on a large Open Source project that had it preconfigured into the build. I had to a) install it and b) ended up filing a PR on the linter's project as it was crashing (due to a force unwrap IIRC). I personally don't feel the compiler should shy away from linting options just on principle. Internally the compiler has assembled the AST on which such analysis can really be performed definitively to the extent that the PR I have in mind is only 22 lines, I don't think it can be rejected on grounds of risk.

That has been the longstanding principle articulated by the core team through multiple proposals.

3 Likes

@xwu, please give the core team time to speak for themselves if they want to. I didn't follow it but I thought the previous conversations about adding options were about not wanting to open the floodgates to options to turn warnings off as you can with clang for bad imports from Objective-C though there may be other instances (and in general suppressing warnings is a pretty bad idea.) In this case it is an option to opt-into a warning which for me stands out as something people might be advised to use.