Skip to content

proposal: syntax fix for := 'shadow variable' problem #30163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
BrianWill opened this issue Feb 11, 2019 · 9 comments
Closed

proposal: syntax fix for := 'shadow variable' problem #30163

BrianWill opened this issue Feb 11, 2019 · 9 comments

Comments

@BrianWill
Copy link

Problem

The := syntax is error-prone when dealing with multiple targets and variables of enclosing scope:

// often a mistake:
var a int
{
  a, b := foo()    // creates a new 'a' in this scope instead of using 'a' of outer scope
}

// the fix
var a int
{
  var b *Bar
  a, b = foo()
}

Not only is the fix verbose, it defeats a nicety of inferred typing that it often spares us from having to remember precise return type(s), e.g. whether a returned type is a pointer or not:

a := bar()       // whether bar() returns an *X or just an X, we often don't need to care

Normally, Go disallows redeclarations, but it makes an exception for the := syntax, such that we can't look at an := assignment and know for sure which of the target variables are being declared:

x, y, z := foo()    // only one of x, y, and z must be new, but just looking here doesn't tell us which variables are new

Another problem is that we can't mix 'name' targets and 'non-name' targets:

a, b[0] := foo()          // compile error: b[0] is a non-name target

Proposal

A syntax that marks individual targets of assignment as new variables. Let's say for now we'll use ' as a suffix marker:

x', y, z' = foo()         // assign to new variable 'x', existing variable 'y', and new variable 'z'

We wouldn't remove the existing := syntax, of course, but in time its use would be considered non-standard style.

With this marking syntax, I don't see a good reason to disallow mixing in non-name targets:

a', b[0] = foo()          // ok

It would also be nice if we could optionally specify types of one or more of the newly declared target variables to make them different from the corresponding return type:

a' Fruit, b = bar()       // first return type of bar() is Banana, but we store it in a Fruit interface variable

As a last little bonus, perhaps type casts could be allowed on assignment targets:

a' int64(), b' = foo()     // the int64() indicates a cast of the return value assigned to 'a'

//spares us from having to create an additional variable of the type we don't want:
notA', b' = foo()
a' = int64(notA)

I think these syntax choices are compatible with the existing grammar, but regardless, I'm not strongly attached to this exact syntax.

@gopherbot gopherbot added this to the Proposal milestone Feb 11, 2019
@brhx
Copy link

brhx commented Feb 11, 2019

I like this proposal a lot. I understand you're not massively attached to that syntax, what about appending or prepending +? It indicates the addition of a new variable and I'm not aware of any clashes with existing grammar.

a, +b = foo()
a, b+ = foo()

@zigo101
Copy link

zigo101 commented Feb 11, 2019

Similar to this? #377 (comment)

BTW, maybe it is not a big problem if the rules are defined clearly.
But sometimes, it may hurt the readability.

func f() {
	var a = []int{1, 2, 3}
	{
		a', a[1] = map[int]int{}, 999 // Which variable the second a represents?
		                              // It would be better to represent the old one,
		                              // but may bring a little confusions.

		fmt.Println(a) // map[] or map[1:999]?
	}
	fmt.Println(a) // [1 2 3] or [1 999 3]?
}

@BrianWill
Copy link
Author

BrianWill commented Feb 11, 2019

@sam3d I lean towards ' because it's easy to type, isn't used for anything else except character literals (which are generally distinctly highlighted), and stands out within code. Also x+ = 3 looks too much like x += 3, so +x = 3 would probably be the better option of the two. Again though, I think + looks too much like a lowercase letter and so doesn't stand out enough. A colon prefix might be the next best candidate: :x = 3

@go101 I'd say that's a niche enough scenario that simultaneous declaration and use in different targets of the same assignment should just be a compilation error. Can you think of any idioms where lack of some allowance would be missed?

@zigo101
Copy link

zigo101 commented Feb 11, 2019

A compilation error? So do you think the second a in the assignment should be the new declared a?
I think this is a bad design. It means the scope of the new declared a starts from the comma following the a identifier.

Could you clarify which ones of the following as in the assignment line are the old one, and which ones are the new one?

var a = []int{1, 2, 3}
{
    a[1], a', a[1] = a[0], []int{7. 8. 9}, a[0]
}

@DeedleFake
Copy link

DeedleFake commented Feb 11, 2019

An alternative fix for the example problem in the proposal which fixes the typing problem in some circumstances but not the verbosity issue is to purposefully use a third variable:

var a int
{
  tmp, b := example()
  a = tmp
}

@OneOfOne
Copy link
Contributor

Please don't do that, don't bring Rust's crap syntax into go.

@ianlancetaylor
Copy link
Contributor

@OneOfOne Please stay polite. Thanks.

The ideas here seem very similar to ideas already expressed in #377, so closing this issue in favor of that one.

@pam4
Copy link

pam4 commented Feb 11, 2019

EDIT: sorry, I didn't realize the issue was closed.

This proposal is just another variation of those in issue #377 (some of them are 8 years old), and the discussion should probably take place there.

And yes, I'm totally in favor of a per-variable short declaration syntax, I already commented here.
Multivariable := is the single thing about Go that drives me crazy, in fact I wrote a simple code rewriter so that I can do without := altogether.

@go101 my code rewriter turns each left hand side element into its own statement in order, so:

var a = []int{1, 2, 3}
var b = []int{1, 2, 3}
{
    :a, a[1] = map[int]int{}, 999
    fmt.Println(a) // map[1:999]

    b[1], :b = 999, map[int]int{}
    fmt.Println(b) // map[]
}
fmt.Println(a) // [1 2 3]
fmt.Println(b) // [1 999 3]

@DeedleFake I used the temporary variable solution for years (with subsequent assignments on the same line for clarity, but gofmt breaks it up :(, and there are other problems too).

var a int
{
    A, b := example(); a = A
}

@BrianWill
Copy link
Author

@OneOfOne I don't see how my suggestion is like Rust

@go101 I'm saying you just have to pick a different name for the new variable:

var a = []int{1, 2, 3}
{
     // a[1], a', a[1] = a[0], []int{7. 8. 9}, a[0]           // compile error
    a[1], b', a[1] = a[0], []int{7. 8. 9}, a[0]               // ok
    a' = b
}

Come to think of it, I'm not happy that Go allows redeclaration of a variable from outer scope that has already been used in this scope:

a := 3
{
    foo(a)    // shouldn't be able to use 'a' of outer scope here
    a := 5    // because we redeclare it later in this scope
    foo(a)
}

This seems error-prone to me as well, but it's too late to change this.

@golang golang locked and limited conversation to collaborators Feb 12, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants