Skip to content
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

proposal: spec: let := support any l-value that = supports #30318

Open
bradfitz opened this issue Feb 19, 2019 · 49 comments
Open

proposal: spec: let := support any l-value that = supports #30318

bradfitz opened this issue Feb 19, 2019 · 49 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@bradfitz
Copy link
Contributor

bradfitz commented Feb 19, 2019

(Pulling this specifically out of #377, the general := bug)

This proposal is about permitting a struct field (and other such l-values) on the left side of :=, as long as there's a new variable being created (the usual := rule).

That is, permit the t.i here:

func foo() {
    var t struct { i int }
    t.i, x := 1, 2
    ...
}

This should be backwards compatible with Go 1.

Edit: clarification: any l-value that = supports, not just struct fields.

/cc @griesemer @ianlancetaylor

@bradfitz bradfitz added LanguageChange Suggested changes to the Go language v2 An incompatible library change labels Feb 19, 2019
@gopherbot gopherbot added this to the Proposal milestone Feb 19, 2019
@ianlancetaylor ianlancetaylor changed the title proposal: go2: permit struct field assignment with := proposal: Go 2: permit struct field assignment with := Feb 19, 2019
@robpike
Copy link
Contributor

robpike commented Feb 20, 2019

I think instead we should aim to eliminate redeclaration, which becomes much less compelling if we can get to a smoother error handling model. Won't happen soon though.

Remove features rather than add them.

@ianlancetaylor
Copy link
Contributor

I think that eliminating redeclaration is a good path, but I'm not sure it affects this proposal. This basically says that you can write

    // Declare err, assign to s.f and err.
    s.f, err := F()

@josharian
Copy link
Contributor

Why only struct fields? slice[i], array[i], map[k]?

Remove features rather than add them.

Removing restrictions can be a net increase in simplicity, if they result in a more uniform application of rules. This proposal would reduce some differences between what can be on the LHS/RHS of = and :=. (Although it is not obvious to me whether describing that set of differences gets easier or harder.)

@bradfitz
Copy link
Contributor Author

bradfitz commented Feb 20, 2019

Yeah, sorry, I oversimplified. Any L-value that works with =.

@zigo101
Copy link

zigo101 commented Feb 20, 2019

And *pointer.

If redeclaration is removed, then a if block would become:

    if var x, y = f(); x == y {
        ...
    }

good? Personally, I can accept it.

@griesemer
Copy link
Contributor

@go101 This is not about removing ":=" (which we definitively want to keep), but about the ability to redeclare a previously declared variable (which one can only do using ":=").

@networkimprov
Copy link

@robpike, how would you mitigate the millions (?) of lines of code that would be broken by eliminating assignment redeclaration?

@bradfitz bradfitz changed the title proposal: Go 2: permit struct field assignment with := proposal: Go 2: make := support any l-value that = supports Feb 20, 2019
@bradfitz bradfitz changed the title proposal: Go 2: make := support any l-value that = supports proposal: Go 2: let := support any l-value that = supports Feb 20, 2019
@bradfitz
Copy link
Contributor Author

@networkimprov, slightly off topic, but when we do remove language features, the plan is outlined in https://github.com/golang/proposal/blob/master/design/28221-go2-transitions.md ... language features can be removed (for a user declaring a certain language version), but we can't change language semantics and silently alter programs.

But this isn't a bug about removing language features. This is strictly about increasing the number of programs that are accepted and making := behave more like =.

@beoran
Copy link

beoran commented Feb 20, 2019

I think :=is already too powerful and complex as it stands. While now, due for error handling I also use it in the form newvar, err := somefunc(), it's not very clean that only some of the variables are newly defined while others merely get assigned . If the proposals for better error handling make that form unneccessary, then we can upgrade the millions of lines of existing code like that with go fix.

In my mind 'x :=' should be semantic sugar for 'var x ='. Allowing more complex expressions on the LHS only adds to the confusion :=can create, which is why I respectfully ask that this proposal be rejected.

@pam4
Copy link

pam4 commented Feb 20, 2019

Possible duplicate of #6842 (but #6842 only talks about fields).

I don't see this as a feature, but rather something about allowing a more general behavior.
In the code: var x int; x, y := f(), in Go terminology we say that we are redeclaring x, but programmers are more likely to think in terms of reusing the previously declared x.

The := behaves like a var for y, because var y would be allowed there, but it behaves like regular = for x, because var x would not be allowed there.
The second case could be made to include other kinds of l-values for the same principle, without adding complexity.

But if this proposal is accepted, we really don't need redeclarations any more, because we can just turn the variables we want to reuse into expressions by enclosing them in parentheses.
This would not just spare us a few long declarations. It would finally make it clear, in a multivariable :=, which variables are new and which are not (imho effectively solving #377).

@bradfitz
Copy link
Contributor Author

Oh, yes, this is #6842. But #6842 was closed, folded into #377. Amusingly, I was asked to create this bug because #377 was too crowded and hard to discuss.

@josharian
Copy link
Contributor

One interesting comment from #6842 worth duplicating here, by @ascheglov:

A slightly different case is when it happens in the if statement: if x.f, ok := f(); !ok { You usually want that ok variable visible only inside that if statement, and you don't want to declare it in outer scope.

There is indeed a bit of a scope problem there.

@bradfitz
Copy link
Contributor Author

@josharian, this proposal doesn't change what would happen to ok in that code.

What's the scope problem? That it permits assigning to x.f where x is not in a private scope specific to that if body?

@josharian
Copy link
Contributor

Yeah. That’s a significant behavior change from before.

(The alternative is to have the assignment to x.f be temporary to that scope, and reverted afterwards, which would just be weird. But would also be analogous in some ways what would happen with a shadowed variable: https://play.golang.org/p/BtdQSLQGh-e.)

@bradfitz
Copy link
Contributor Author

I guess I don't see that as an important or significant behavior change. That's behaving exactly as this bug is about.

@pam4
Copy link

pam4 commented Feb 20, 2019

Yeah. That’s a significant behavior change from before.

@josharian, you are probably making some assumption about programmer expectations that I can't see (also I don't see any compatibility issue).

Obviously in the code: *f(x), y := g() I would not expect anything strange to happen to f or x; it wouldn't make any sense.

The principle for := would be: if you cannot make a var of an element, then such element is just assigned to.
If you think of redeclared variables as if they were "just assigned to" (the effect is the same), this principle already describes current := behavior.

var x int
x, y := f() // "var x" not allowed here -> just assign to x

@josharian
Copy link
Contributor

Yes, I was thinking about programmer expectations. I find the if x.f, ok := f(); !ok { example surprising, but perhaps others don't. That's fine.

@networkimprov
Copy link

networkimprov commented Feb 20, 2019

To a dev used to javascript, these would seem to add an element to a container (EDIT: and declare a variable). I think @beoran has a point.

t.m,  x := f()
s[i], x := f()  // slice
m[k], x := f()  // map; really can add an element :-)

@bradfitz
Copy link
Contributor Author

@networkimprov, I don't follow.

@DeedleFake
Copy link

DeedleFake commented Feb 20, 2019

I think he meant that there's a difference between a struct field, a slice index, and a map index on the left-hand side of a :=. A struct field and a slice index both require the field and index to actually exist, although the struct field is checked at compile-time and the slice index is checked at runtime. A map index, on the other hand, adds something to the map if you assign to an index that doesn't already exist. Conceptually, there is a difference here, and since := is a combo declaration and assignment I think he's saying that he thinks it's confusing that only one of those actually creates something new rather than reassigning an existing item.

I don't think it makes a whole lot of difference, however. The three cases would, I assume, work exactly the same way with := if they were allowed alongside a new variable declaration as they already do with =.

The JavaScript reference is probably in reference to the fact that, in JavaScript, objects are actually maps with a few extra features and you can assign to an array element that doesn't exist yet, which automatically fills in the rest of the array. If you tell someone who's used to JavaScript that := is a declaration and then also allow those assignments on the left-side, as proposed here, they may find it confusing that it doesn't work the same as in JavaScript. I highly doubt it would be much of an issue, however. They also have to learn how a type system works, along with anything else that's different, so...

@networkimprov
Copy link

networkimprov commented Feb 20, 2019

@bradfitz, mixing assignment and declaration can be confusing. The assignments in the stmts I listed could appear to be declarations affecting a container.

It's allowed for x, err := f() because that's a pure declaration in some cases, and because there isn't a scheme to dispatch errors to handlers in Go1.

@bradfitz
Copy link
Contributor Author

With this proposal, it's obvious from visual inspection that the proposed new LHS forms are not declarations if they have any punctuation at all.

@SaleProperly
Copy link

Personally, I love this idea and think it makes the code more readable.

var err
s.x, err = f()

now becomes

s.x, err := f()

I think this behavior is predictable, readable, and expected. Given that we can already redeclare/reassign in other more limited circumstances.

It would be clear to the reader that something on the left is being declared as we are using :=

It would also be clear that s.x (or any other more complicated expression) is being reassigned.

IMO, this allows more code to be written using an expected go idiom.

@bogdanpetrea
Copy link

This problem has a nice solution somebody proposed in 2010, in the previously mentioned issue: #377 (comment)
I also had the same idea recently, so I guess it wouldn't be unintuitive. It's basically:

func foo() {
    var t struct { i int }
    t.i, :x = 1, 2
    ...
}

@zigo101

This comment was marked as off-topic.

@ianlancetaylor

This comment was marked as resolved.

@zigo101

This comment was marked as off-topic.

@ianlancetaylor

This comment was marked as resolved.

@scorsi
Copy link

scorsi commented Jul 2, 2020

There's just one thing that I dislike, here the example:

type A struct {
  x int
}

func myErrorFunc() (int, error) {
  return 0, fmt.Errorf("oups")
}

func doSomething(a *A) error {
  a.x, err := myErrorFunc()
  if err != nil {
    return err
  }
  return nil
}

func main() {
  a := A{x: 42}
  err := doSomething(&a)
  if err != nil {
    // do something with a.x but... it was changed and we lost our data...
  }
}

To fix this issue, we could do:

if tempX, err := myErrorFunc(); err != nil {
  return err
} else {
  a.x = tempX
}

We'll finally fallback to the old syntax or create temporary structs/slices/maps to avoid altering data in case of errors...

@icholy
Copy link

icholy commented Jul 27, 2020

@scorsi If you assign a value to a variable, you will lose the old value. I don't see how that's specific to this proposal.

x := 42
var err error
if x, err = strconv.Atoi("bad"); err != nil {
    // do something with x but... it was changed and we lost our data...
}

@mdempsky
Copy link
Contributor

This seems like a popular issue. Do folks have examples of real world code that this would benefit?

I frequently need to use = instead of := because the latter wouldn't scope variables how I intended. But I can't think of any times I've had to do it because := doesn't support assigning to struct fields / array elements / etc.

My experience is most multi-valued functions include a return value that provides information about the other returned values; e.g., an error or ok bool. Further, that when calling these functions, I want to check this value before storing the other values somewhere.

I'm curious to hear about others' experiences here, and any common use cases I'm not aware of.

--

From an implementation point of view, this seems easy enough.

I haven't seen any mention of what should happen if a redeclared variable appears in an LHS expression. For example:

p := new(int)
{
    p, *p := new(int), *p + 1
    println(*p)
}
println(*p)

Does this program compile? If so, what does it print?

My assumption would be LHS expressions should be evaluated in the same scope as RHS expressions (i.e., based on identifier bindings before they're re-bound). That is, it would be equivalent to:

p0 := new(int)
{
    var p1 *int
    p1, *p0 = new(int), *p0 + 1
    println(*p1)
}
println(*p0)

and thus print 0 1.

@icholy
Copy link

icholy commented Aug 22, 2020

My experience is most multi-valued functions include a return value that provides information about the other returned values; e.g., an error or ok bool. Further, that when calling these functions, I want to check this value before storing the other values somewhere.

@mdempsky It comes up often when parsing something into a struct. If an error is encountered, the whole thing is thrown away anyway.

func readThing(r *bufio.Reader) (*Thing, err) {
    var t Thing
    if t.A, err := readA(r); err != nil {
        return nil, err
    }
    if t.B, err := readB(r); err != nil {
        return nil, err
    }
    return &t, nil
}

@pam4
Copy link

pam4 commented Aug 22, 2020

@mdempsky

I frequently need to use = instead of := because the latter wouldn't scope variables how I intended.

IMHO, that's the most important problem this proposal would solve (more details above; if I misunderstood what you mean, perhaps you could provide an example).

file := os.Stdin
if arg != "-" {
    (file), err := os.Open(arg)
    if err != nil {
        //...
    }
    defer file.Close()
}

It is also a better alternative to the infamous "redeclaration" mechanism of :=, and would pave the way for its future removal:

a, ok := f()
if !ok {
    //...
}
b, (ok) := g()

(Again, there is nothing special about parenthesized identifiers, they already work on the LHS of an =. The use of parenthesized identifiers in this fashion is no exception to the basic rule of this proposal: naked identifiers are declared, everything else is assigned to.)

But I can't think of any times I've had to do it because := doesn't support assigning to struct fields / array elements / etc.

It happens to me from time to time. @icholy use case is a good example. Sometimes assigning to a field/element before checking the error is not a problem. And there are multi-valued functions that always succeed.

Does this program compile? If so, what does it print?

I don't see a reason for compiling to fail, and I agree with your assumption. It seems consistent with existing evaluation rules.

@icholy
Copy link

icholy commented Aug 22, 2020

@pam4 does that syntax introduce any parsing ambiguities?

@mdempsky
Copy link
Contributor

Thanks for the example use cases.

@pam4:

IMHO, that's the most important problem this proposal would solve (more details above; if I misunderstood what you mean, perhaps you could provide an example).

No, sorry, I just missed that detail. So to restate this:

This proposal is about extending the LHS of := statements to include any assignable expression. Bare identifiers ("no punctuation" as Brad put it) would continue to have their current behavior, but other expressions would work as in normal assignment statements.

Assignment statements already allow LHS expressions to be parenthesized; e.g., (a) = 1 is valid. So (a), b := f() would therefore also be valid under this proposal. Here a would never be (re)declared (because it involves punctuation), but b would always be declared. (Or there would be a compiler error if b already exists in scope, since := must declare at least one variable).

I think that makes sense. I also agree it would apply to the use case I mentioned. So instead of writing something like:

var f *os.File
if foo {
    var err error
    if f, err = os.Open("foo"); err != nil { ... }
}

I could write:

var f *os.File
if foo {
    if (f), err := os.Open('foo"); err != nil { ... }
}

That looks pretty funny to me at the moment, but maybe it would grow on me. (I've always preferred the x := x trick for working around the scoping issue with go statements inside of loops, even though most other folks seem to prefer go func(x) { ... }(x).)

--

@icholy:

does that syntax introduce any parsing ambiguities?

I'd have to try actually changing one of the parsers to be sure, but I don't immediately see any reason this proposal would cause problems.

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/250037 mentions this issue: cmd/compile, go/parser: relax ":=" statements

@mdempsky
Copy link
Contributor

Yeah, no parser ambiguity from this proposal. The Go parsers already handle = and := identically, and only complain about non-identifiers on the LHS of := later on.

I uploaded a super rough proof-of-concept CL of the current proposal in case anyone wants to play with the idea and maybe demonstrate more real world use cases. It has cmd/compile and go/parser support, so "go fmt" and "go build" should work. It doesn't have go/types support yet, so "go vet" (and thus "go test") will fail.

@connor-hetrafi
Copy link

Having to do

if email, ok := info["email"]; ok {
    user.Email = email.(string)
} else {
    return "", fmt.Errorf("access denied: failed to fetch %s email", provider)
}

instead of

if user.Email, ok := info["email"]; !ok {
    return "", fmt.Errorf("access denied: failed to fetch %s email", provider)
}

breaks my heart.

for anyone wanting another use case

@mdempsky
Copy link
Contributor

@Hetrafi In your first example you have a type assertion to string, but the second example does not. Is that intentional?

I don't think those two programs would be equivalent under this proposal. I'm also inclined to say they should not be equivalent.

@DeedleFake
Copy link

DeedleFake commented Feb 26, 2021

@Hetrafi

The problem is that that shorter one modifies the user struct directly, which means that if there is an error, the caller might get modified unintentionally. I think it's worth having the option, at least, but it's a difference that should be kept in mind.

On a side note, I usually do

email, ok := info["email"]
if !ok {
  return ...
}
user.Email = email

for that situation. It puts email and ok into the scope outside of the if, but it avoids the else.

Edit: I still think that the best solution to this is to separate the declaration from the assignment. Right now you have two options, only assign via =, or declare, assign, and possibly shadow via :=. If the declaration was per-variable on the left-hand side instead of being a different assignment operator, all of the problems that := has would just disappear. No accidental shadowing, and no reason that you couldn't put a struct field or a map index on the left-hand side. I know that there are concerns about readability, but I think that that's something that could probably be solved with a well-designed syntax.

@ianlancetaylor ianlancetaylor removed the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Aug 22, 2023
@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: let := support any l-value that = supports proposal: spec: let := support any l-value that = supports Aug 6, 2024
@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change labels Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests