Note: this is motivated by a conversation in the "deprecating force unwrap" thread where I went a little bit off-topic: Moving toward deprecating force unwrap from Swift? - #73 by Anachron, but I've been thinking about this for a while now.
If you've used Swift for a while, you will almost inevitably fall in love with guard
statements. They are a truly great feature of the language and many people learn to prefer them over if
statements. But why is that?
The cool thing about guard
statements is not so much their inverted semantics and optional binding rules compared to if
statements. Their appeal really comes from the fact that they force you to leave the scope somehow. A reader of your code does not need to look into the else
branch to know: "if this condition fails, I'll leave the current scope in some way". This is a great way to communicate intent more globally.
Here's an edge case that gives people headaches:
guard error == nil else {
throw error! //force unwrap necessary :(
}
If you want to avoid the force unwrap, you need to write:
if let error = error {
throw error
}
But then, you lose this nice added benefit of the guard
statement! Now, there's an obvious "solution" for that so you can still communicate scope-leaving-behaviour more globally: just wrap the rest of your function into an else
-block. However, if you have a really unpleasant case and you need many variables to be nil and you need to execute some code in between so that covering them with else if let
is not an option, you'll quickly end up with a pyramid of else
clauses.
Granted, such scenarios probably mean that you have deeper problems with your code, but the mere possibility of having to write deeply nested else
s is troubling and I tend to feel awkward with even one if let ... else
.
If you think about the problem further, you notice: this is actually not just a problem of if
s or if let
s, but of virtually all control flow statements (except guard
): if you read any of for in
, switch
, while
or while let
, you actually need to look into the bodies of those blocks to figure out if some, all or none of their branches leave the current scope.
A general solution is therefore desirable.
What I thought of is a set of annotations: one indicating "this control flow statement never leaves the current scope" (likely used in imperative code with while
and for in
), one indicating "this control flow statement sometimes leaves the current scope" (in mixed situations) and one indicating "this control flow statement always leaves the current scope" (likely used in switch
statements or the above if let
). Applying an incorrect annotation (except for the "sometimes" annotation) would be a compile-time error.
The question, however, is: what about backward compatibility? Either these annotations are opt-in, in which case you'd get a lot of compiler warnings that get very annoying quickly and you don't see the real problems anymore or there would be no enforcement at all and it would be just a nice gimmick for people who love structured programming. A third solution could be that the annotations are generated fully automatically and shown to the user without being part of the actual source-code, but that would be a bit of a paradigm shift and it would move this an IDE feature rather than a compiler feature.
Probably, the no-enforcement policy would be best as long as you can opt-in any enforcement rules using a linter.
Thoughts? Ideas? Am I missing something?