tera
1
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
}
2 Likes
ktoso
(Konrad 'ktoso' Malawski 🐟🏴☠️)
2
Without any historical info about the feature… I’m anyway strongly inclined to say it’s a feature — especially for unwrapping optionals 
2 Likes
jrose
(Jordan Rose)
3
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.)
2 Likes
Nevin
4
On swift.godbolt.org this code:
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.
4 Likes
scanon
(Steve Canon)
5
We could make a language change to allow this if we wanted to. I'm not sure if I think it's more consistent or not, though.
1 Like
Nevin
6
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.
7 Likes
tera
7
It's not a question that guard let x = x shall be allowed when the right x is in the outer scope:
func foo(x: Int?) {
guard let x = x else { fatalError() }
}
func foo() {
var x: Int? = ...
do {
guard let x = x else { fatalError() }
}
}
only a question if it's in the same scope. The following looks like a back door:
let v = 1
// let v = "2" is not allowed here but we can cheat:
guard let v = .some("2") else { fatalError() }
If that's a feature - that's totally fine with me; just wanted to brought this example to the light in case it is unintentional / bug / regression.
Nevin
8
I’m saying the intended use is like this:
func foo(_ n: Int) -> Int? {
let x = longExpression.fooImpl(n)
guard let x = x else { return nil }
return additionalCalculation(x)
}
1 Like
tera
9
If we didn't have this feature, we'd still be able doing it:
func foo(_ n: Int) -> Int? {
guard let x = longExpression.fooImpl(n) else { return nil }
return additionalCalculation(x)
}
Jumhyn
(Frederick Kellison-Linn)
10
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...
1 Like
tera
11
You can redefine function parameter with a mere let/var ! (function parameters are in a different scope):
func foo(x: Int) {
var x = x
}
2 Likes
Jumhyn
(Frederick Kellison-Linn)
12
Yeah, that's what I was getting at by "not quite equivalent."
Nevin
13
As I said, I believe the reason this feature exists is specifically to let people write code the way I wrote it, *instead of* the way you wrote it.
xwu
(Xiaodi Wu)
14
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.
11 Likes
Zollerboy1
(Josef Zoller)
15
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.
tera
16
This simple example shows that guard doesn't introduce a new scope:
func foo() {
var x: Int? = ...
let y = 0
guard let x = x else { fatalError() }
let y = 1 // error
}
5 Likes
that explains a lot, i could have sworn i remembered seeing compiler errors for this in the past… glad i'm not going crazy
1 Like