Variable redefinition via guard

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

Without any historical info about the featureā€¦ Iā€™m anyway strongly inclined to say itā€™s a feature ā€” especially for unwrapping optionals :slight_smile:

2 Likes

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

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

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

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

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.

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

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)
}

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

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

Yeah, that's what I was getting at by "not quite equivalent."

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.

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

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.

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