Is it a bug or a feature that I can redefine a variable in the same scope via guard? Or is the scope different here?(the scope is the same here, you can check this by defining another unrelated variable with "let x = 0" and trying to redefine it with another "var x = 0" after the guard line).
func foo() {
var a: Int? = 1
var b: String? = "2"
var c: Double? = 3.14
let v = a
// let v = b // ok: Invalid redeclaration of 'v'
// let v = c // ok: Invalid redeclaration of 'v'
guard let v = a else { return } // ok?!
print(v) // 1
guard let v = b else { return } // ok?!
print(v) // "2"
guard let v = c else { return } // ok?!
print(v) // 3.14
}
Edit: another example along the same lines:
func foo() {
let v = 1
print(v) // 1
// let v = "2" // can't do this... but can do something to the same effect:
guard let v = .some("2") else { fatalError() }
print(v) // "2"
// let v = 3.14
guard let v = .some(3.14) else { fatalError() }
print(v) // 3.14
}
Iām going to say bug but not one we can ever fix, because surely people are relying on it by now. If it were intended youād be able to shadow bindings in the same scope with a plain let, as you tried. (By comparison, Rust does allow that.)
let x: Int = 1
guard let x: String = Optional("a") else { fatalError() }
At global scope compiles on all available versions of the Swift compiler (going back to 3.1.1).
However, that exact same code inside a smaller scope (eg. a do block or a function body) first began compiling in Swift 5.4.
So I surmise that it was always meant to be valid, and the fact that it was not valid in nested scopes through Swift 5.3 was a bug which has now been fixed.
I suspect the reason for making it work as it does, is specifically to enable people to write guard let x = x.
As in, the only ārespectableā (in the sense that the language respects it) use of same-scope shadowing, is to safely unwrap an optional.
The fact that one can write guard let x = y when thereās already an x declared in the same scope is perhaps more of a fluke / side effect / unintentional consequence of the implementation.
But being able to write guard let x = x seems deliberate.
There's a commit as early as 7 years ago which uses the guard let a = a pattern. Unfortunately, it's shadowing a function parameter, so not quite equivalent to shadowing a variable in the same scope, but it does at least imply that guard let a = a and if let a = a are expected to behave the same with regards to shadowing...
One doesnāt need to invoke longExpression to glimpse at why the feature in question is supported. An adequate observation is this:
The raison dāĆŖtre of adding guard to the language was to allow users to avoid the āpyramid of doomā with nested if statements.
It makes sense to allow users to be able to adopt guard easily (mechanically, even) where prior to the introduction of guard they had a bunch of nested if statements.
Since it is permissible to shadow a variable from an outer scope in an inner scope, users could (and still can) nest if let x = ā¦ arbitrarily many times.
It removes an additional barrier to adoption of guard if it supports the same let bindings without requiring users to jump through the hoop of also renaming their variables when they refactor.
Even if the actual compiler implementation works differently (which it certainly does), there is a nice mental model that shows why guard works the way it does.
If we look at the following guard statement:
guard someCondition else { return }
// do something
This is actually syntactic sugar for the following:
if someCondition {
// do something
} else {
return
}
Notice, that our code after the guard implicitly sits inside of a new scope.
Therefore I would say that guard does actually introduce a new scope, even if it's not really visible like other scopes in Swift.
If we rewrite your example using this implicit meaning of guard, we go from this:
to this:
func foo() {
var a: Int? = 1
var b: String? = "2"
var c: Double? = 3.14
let v = a
// let v = b // ok: Invalid redeclaration of 'v'
// let v = c // ok: Invalid redeclaration of 'v'
if let v = a {
print(v) // 1
if let v = b {
print(v) // "2"
if let v = c {
print(v) // 3.14
} else {
return
}
} else {
return
}
} else {
return
}
}
Which is much harder to read but makes clear that we aren't actually declaring new variables with the same name in the same scope over and over again, but that we actually have a new (implicit) scope for each guard statement.