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: Go 2: let := support any l-value that = supports #30318

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

Comments

@bradfitz
Copy link
Member

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

@gopherbot gopherbot added this to the Proposal milestone Feb 19, 2019

@gopherbot gopherbot added the Proposal label 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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

commented Feb 20, 2019

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

This comment has been minimized.

Copy link
Contributor

commented Feb 20, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

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

@go101

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

commented Feb 20, 2019

@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

This comment has been minimized.

Copy link

commented Feb 20, 2019

@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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

@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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

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

This comment has been minimized.

Copy link
Contributor

commented Feb 20, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

@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

This comment has been minimized.

Copy link
Contributor

commented Feb 20, 2019

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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

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

@pam4

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

commented Feb 20, 2019

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

@networkimprov, I don't follow.

@DeedleFake

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member Author

commented Feb 20, 2019

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.

@baryluk

This comment has been minimized.

Copy link

commented Mar 21, 2019

So, I kind of stumbled on this entire non-name x.y on left side of :=, when I tried:

reply.Data, found := someMap[request.Key]
if !found { return notFoundError }

In this particular case I do not care what happens when the key is not found, and I am fine with reply.Data being overwritten or be an empty of some sorts (in my case Data is of a struct type). However, some people might have an expectation that if the value was not found, it is not being assigned to reply.Data:

a[k], err := f(k)

It is very common for f(k) to return no or empty data and error, on error, and in many cases user do not want assignment to a[k] to be done on error. However, adding rules for this will make semantic riddled with possible special cases, which is not a good thing.

As much as I would like to have such functionality, error handling first should be tackled in the first place, and then this topic covered later, otherwise it might not align with general handling techniques.

PS. I do not even know what exactly is going to happen here right now, and I simply avoid this construct, because I do not care to remember all spec details all the time:

var found bool
a[k], found = b[k]
@networkimprov

This comment has been minimized.

Copy link

commented Mar 22, 2019

There are good arguments for eliminating assignment from := statements (so they're purely declarative).

The Go 2 Draft Design for Error Handling suggests that assignment by := be dropped. Its model hides error return values -- the primary reason that assignment by := was adopted.

Go takes one of eight paths for a, b := f(), as each var is defined, assigned, or shadowed. One cannot tell which without searching prior code, so it's easy to write something incorrect. Dropping assignment cuts the number of paths to four.

@pam4

This comment has been minimized.

Copy link

commented Mar 25, 2019

@networkimprov, I assume that by "assignment" you mean "redeclaration" (I also think of it as an assignment, but in Go terminology it's a redeclaration).

I think dropping redeclarations would only make sense if this proposal is also accepted. Otherwise why have := at all?
Redeclarations are the defining characteristic of :=. It's not type inference; many users forget that var has type inference too.
Without redeclarations, := would be no more than a shorthand to save exactly 3 characters
(var a, b = f() -> a, b := f()).
Conciseness is good, but I don't think these 3 characters really make a difference.
Dropping redeclarations would be just as backward incompatible as dropping := altogether, but it would make for a more painful transition (old style code harder to spot and more confusion).
I think @ianlancetaylor motivation for closing #29081 is pretty weak if dropping redeclarations is ok.

However, if we trade redeclarations for something better (i.e. this proposal), we can make := useful again, taking full advantage of type inference without giving up explicitness.

a, b := f()
a, c := f()   // comp. error: "a redeclared in this block"
(a), d := f() // assign to a; new d
{
    (a), d := f() // assign to a (from the outer block); new d (shadowing)
}

There's nothing special about parenthesized identifiers, they are normal expressions. As @bradfitz put it: "it's obvious from visual inspection that the proposed new LHS forms are not declarations if they have any punctuation at all".

I also don't see any interference with the error handling draft.
I still like the colon-prefix syntax better (backward compatible), but I would settle for this one and be happy with it.

@networkimprov

This comment has been minimized.

Copy link

commented Mar 27, 2019

Related: #31064 - cmd/vet: require explicit variable shadowing

@baryluk

This comment has been minimized.

Copy link

commented Mar 27, 2019

There are good arguments for eliminating assignment from := statements (so they're purely declarative).

I can agree with that. I wouldn't be against removing := from the language, if it helps resolve some ambiguities.

Without redeclarations, := would be no more than a shorthand to save exactly 3 characters
(var a, b = f() -> a, b := f()).

I didn't know that this even possible. Or maybe I did know, but didn't make connection. To a person coming from C / C++ background, it feels in var a, b = f(), only b would be assigned/initialized. Sure, after second glance it is obvious, there must happen something to a to, but it is not obvious if it will get initializer from f(), or compiler will complain about lack of one. :)

I also don't see any interference with the error handling draft.

Technically there is no interfering, but because error handling and 'err' reassigning is extremal common pattern related to this bug, the error handling is driving a design here, but that is not good idea, if the entire error handling draft thingy will remove all the need for reassignment of err.

I still like the colon-prefix syntax better, ...
I just checked it. I like it, but doesn't feel Go-like, and is a bit esoteric. I am not a fan of having symbols like colon be used often - it makes code visually noisy (Just look at Perl or some crazy C++).

@DeedleFake

This comment has been minimized.

Copy link

commented Mar 27, 2019

I just checked it. I like it, but doesn't feel Go-like, and is a bit esoteric. I am not a fan of having symbols like colon be used often - it makes code visually noisy (Just look at Perl or some crazy C++).

I'd actually thought of this syntax before. Interesting to see that it was proposed all the way back in 2010.

I agree that it makes it a bit noisy, but I think the general idea here isn't the specifics of using a colon but more the idea of marking the variables being created, rather than trying to get the language to guess by using a different assignment operator. The colon's just an extension of the existing colon-equals syntax. I think that the potential of fixing all of the problems with shadowing is worth the slight noisiness increase.

@pam4

This comment has been minimized.

Copy link

commented Apr 2, 2019

Technically there is no interfering, but because error handling and 'err' reassigning is extremal common pattern related to this bug, the error handling is driving a design here, but that is not good idea, if the entire error handling draft thingy will remove all the need for reassignment of err.

Yes, err reassigning is an extremely common pattern, but I don't think removing such pattern is going to significantly reduce the problem's importance.

A Go function or method can return multiple values, but currently there's no way to mix declarations and non-declarations in the same LHS tuple.
As a result, if some but not all of such return values need new variables, you are stuck with 2 non-optimal solutions:

  1. use temporary variables, which introduce noise and naming problems;
  2. give up type inference, which is also intended to reduce noise.

Even with redeclarations, this situation is already common enough to be annoying for me, in fact I often find myself rearranging code in ways that I shouldn't, just because I don't want to give up type inference.
In a language with type inference, why should I ever need to write the type of a variable that I'm making just to store a returned value? (Unless I want a different type, of course.)

Redeclarations are a bad solution to this problem: insufficient, because the problem is still there, just less frequent, and harmful, because they come with their own set of problems.

If we introduce the new error handling design and drop redeclarations, it wouldn't be harmful anymore, but it would still be insufficient (even more so, for example the "comma ok" idiom would not be covered).

The problem can really be solved only by a mean to explicitly request assignment or declaration independently for different elements of the same LHS tuple. This proposal is one, the colon-prefix syntax is another, and there are many variations.

Everyone has its own (partly subjective) opinion about how they looks visually, but they give you important and concise information about the user's intentions.
Conversely writing a type that could have been inferred, really is just noise because it doesn't tell anything useful.

@deanveloper

This comment has been minimized.

Copy link

commented Jun 3, 2019

I'm not sure if I like the idea of wrapping parentheses around assignments, the main reason being that it would be hard to search for if someone wanted to figure out what it meant. For instance if someone saw:

x, ok := m["foo"]

// ...

y, (ok) := m["bar"]

How would they figure out what (ok) means? I had a similar problem with JavaScript the other day, when I saw code similar to the following:

var foo = 5

// ...

return { foo, bar }

I was very confused as to what { foo, bar } meant. Obviously it's a JavaScript object, but in my eyes it was shorthand for { "foo": undefined, "bar": undefined } or something similar. In reality, it is shorthand for { "foo": foo, "bar": bar }.

I wasn't able to do a search for it either. It's a niche feature so most searches for different ways to declare javascript objects don't go over it. That's really my only issue in allowing something like y, (ok) := m["bar"].

@miladrahimi

This comment has been minimized.

Copy link

commented Jun 28, 2019

It's important to have a solid definition for each element in the syntax. The definition of := is clear and some hacks that change its behavior shouldn't come in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.