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: allow & as l-value operator #40708

Closed
arnottcr opened this issue Aug 12, 2020 · 19 comments
Closed

proposal: Go 2: allow & as l-value operator #40708

arnottcr opened this issue Aug 12, 2020 · 19 comments

Comments

@arnottcr
Copy link
Contributor

@arnottcr arnottcr commented Aug 12, 2020

background

Currently you can use the deref operator, *, to assign a T to a *T:

type Uint64 uint64

func (u *Uint64) unmarshal(s string) (err error) {
        *u, err = strconv.ParseUint(s, 10, 0)
        return
}

However there is no way to deref a returned value:

type Struct struct { url.URL }

func (s *Struct) unmarshal(s string) (err error) {
        var u *url.URL
        u, err = url.Parse(s)
        s.URL = *u
        return err
}

description

I propose we allow the reference operator, &, to be applied to l-values:

func (s *Struct) unmarshal(s string) (err error) {
        &s.URL, err = url.Parse(s)
        return err
}

costs

This adds a new operator for l-values, but the & operator has a consistent value, so it seems intuitive. Currently, this can probably only be added to =, since := does not support *, see #30318.

This should be backwards compatible with Go 1.

@gopherbot gopherbot added this to the Proposal milestone Aug 12, 2020
@gopherbot gopherbot added the Proposal label Aug 12, 2020
@davecheney
Copy link
Contributor

@davecheney davecheney commented Aug 12, 2020

Aside from saving one line, what would this allow go programmers to do that they cannot do today?

@arnottcr
Copy link
Contributor Author

@arnottcr arnottcr commented Aug 12, 2020

The benefit I see is that it makes for a simpler mental model. Much like how there is interest to make = work like :=, I am interested to make & work for l-values and r-values, so that ones intuition about how things works is correct.

You are not wrong that in the provided example it only saves one line, and you can always define more stack variables to solve a problem, but I think there is an elegance to direct assignment that is valuable.

Also, I would ask why we allow * on the left hand side, as by your reasoning it is not required either. This argument has problematic extremes: why do we need methods, when we can just use functions.

@davecheney
Copy link
Contributor

@davecheney davecheney commented Aug 12, 2020

Alternatively the type of s.URL could be changed to avoid having to add a new syntax.

@arnottcr
Copy link
Contributor Author

@arnottcr arnottcr commented Aug 12, 2020

There are valid reasons one may not want to use pointers. url.URL is just an example, something similar can be conceived for other types. I tend to use value types to enforce immutability, but I have also been burnt by pointers to primitives.

That being said, there may also be an opportunity for the compiler to optimise &s.URL, err = url.Parse(s) in a way that it cannot for u, err := url.Parse(s); s.URL = *u, but that may also be nonsense.

@davecheney
Copy link
Contributor

@davecheney davecheney commented Aug 12, 2020

I can’t see an argument for not changing the fields type to make the code read better. It can’t be for performance because if it was the copy from the dereferencing u will dominate.

This just doesn’t feel like a change that would improve Go enough to pay for itself. I think there are more valuable changes that could be made given the extremely conservative stance to adding anything to the language.

@randall77
Copy link
Contributor

@randall77 randall77 commented Aug 12, 2020

I assume from your example that the semantics of

&a = b

is equal to

a = *b

I don't like operators on the LHS that are really operations on the RHS.

Having &a on the LHS makes it look like this is an operation on addresses, but it isn't. It is an operation on values.
In other words, if a and b are of type int and *int, then my intuition says that *ints are flowing across the =, but that's not the case.

@ianlancetaylor ianlancetaylor changed the title proposal: allow & as l-value operator proposal: Go 2: allow & as l-value operator Aug 12, 2020
@ianlancetaylor

This comment has been hidden.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 12, 2020

This seems like a very specific example of the fact that when a function returns more than one result we can't apply an operator directly to the result. For example, one could similarly imagine rewriting

func F1() (int, error) { ... }

func F2() {
    var (i, j int; err error)
    i, err = F1()
    j = -i
}

as

func F2() {
    var (j int; err error)
    -j, err = F1()
}

That is, the same pattern arises for any unary operator. And in fact it arises for any binary operator if we use the += form.

What's special about the unary * operator that it deserves special treatment?

@arnottcr
Copy link
Contributor Author

@arnottcr arnottcr commented Aug 12, 2020

That is fair, I am game for adding other operators (with return values) as valid l-value operators. I just started with something that I saw and had an intuitive and readable solution. You can currently deref l-values, so why not address them too?

If I understand your suggestion correctly, this would involve the following unary operators. I think everything makes intuitive sense and reads pretty well:

var (i int; b bool; p *int; err error)

+i, err = strconv.Atoi("5")         //     5, <nil>
-i, err = strconv.Atoi("5")         //    -5, <nil>
!b, err = strconv.ParseBool("true") // false, <nil>
^i, err = strconv.Atoi("5")         //    -6, <nil>
*p, err = strconv.Atoi("5")
&i = flag.Int("i", 0, "")
c<-, err = strconv.Atoi("5")

The assignment operators have a bit of stutter to them: i+=, err = strconv.Atoi("5") or +=i, err = strconv.Atoi("5"). If this feature was desired, we could instead reserve the right side of left hand operators, since i+, err = strconv.Atoi("5") looks visually similar to i += 5. That being said, this subset is getting pretty out there, and I am not sold on the syntax. However, I am not opposed to their inclusion, but will omit their enumeration for brevity.


On the other side of the argument, since I fear @ianlancetaylor's comment was a straw man, if we are uncomfortable adding any more l-value operators, maybe we should consider removing the ones we have in some future Go2 release. Clearly they are not required, since we can just make more stack variables.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 12, 2020

I dont think we have any l-value operators, at least not in the sense of &s.URL = F(). The left hand side of an assignment is permitted to be any addressable value (or a map index expression or _) (https://golang.org/ref/spec#Assignments). A pointer indirection is an addressable value (https://golang.org/ref/spec#Address_operators).

That is, because we are permitted to write &*p, we are permitted to write *p = F(). This is not inherently different from the fact that we can write s.f = F() for some struct value s.

@martisch
Copy link
Contributor

@martisch martisch commented Aug 12, 2020

To me having all unary operators also on the left side impairs readability as the operation on both left and right side need now be merged mentally to understand how the value of an expression assigned is computed. They might also be visually much further away the current examples. * is special in that regard as it only signifies where to store a value unmodified.

EDIT: I understood the example wrong... the below wont work for multiple returns

If we want to make it easier not to have additional variable declarations, why not (syntax not final):

func (s *Struct) unmarshal(s string) (err error) {
        s.URL, err = &url.Parse(s)
        return err
}

There are proposals for adding & for e.g. struct literals. I feel that would better align with the current way assignments are structured. There are already discussions about the pros and cons of that approach.

@arnottcr
Copy link
Contributor Author

@arnottcr arnottcr commented Aug 12, 2020

Interesting, so I cannot &s.URL = F(), because &&s.URL is invalid. Not exactly how I was thinking about the parser working, but it follows logically. Let me ask a follow up then, why can we not make &s.URL addressable? That would allow for my use case, without building a new spec for l-values.

@arnottcr

This comment has been hidden.

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Aug 12, 2020

Let me ask a follow up then, why can we not make &s.URL addressable?

What would its address be? What would it mean to store a value at that address?

@arnottcr
Copy link
Contributor Author

@arnottcr arnottcr commented Aug 22, 2020

I think I follow your confusion. Assignments currently place a value into a location. My suggestion is to unpack (dereference) a pointer into a location:

// copy the value returned by F() into value p points to
*s.URL = F()
// shallow copy the pointer returned by F() into the temporary pointer of s.URL
&s.URL = F()

I see how that could be unintuitive, if you read it as desugaring into:

p := &s.URL
p = F()Because there is no way to desugar I assume
// s.URL unchanged

But this is like saying that these are equivalent:

*s.URL = F()
// and
v := *s.URL
v = F()

As such there must be custom logic to unpack *p := F() into the following actions:

  • capture the value of F()
  • find the memory location that p points to
  • assign the captured value there

Is it not reasonable to say that &v := F() could unpack similarly:

  • capture the value of F()
  • find the memory location the captured value points to
  • assign that value to v
@arnottcr
Copy link
Contributor Author

@arnottcr arnottcr commented Aug 22, 2020

Looking back at a different example we see that:

var a uint
var b *uint
&a = b // store the value b points to into a
// is equal to
a = *b // get the value b points to and store it into a

Where as:

var a *uint
var b uint
a = &b // get the address of b and store it into a
// is not equal to
*a = b // store the value b into the location pointed to by a

As I see this, in the second case we have one thing var b uint that can either be placed into a two ways, directly or indirectly, thus the two operators can do different things, where as in the first case, var a uint is a value and must accept a uint in assignment, thus the two can be congruent.

To me this makes sense and seems intuitive, since assignment is not equality, but maybe that is not a universal point of view.

@mdempsky
Copy link
Member

@mdempsky mdempsky commented Aug 22, 2020

The nature of assignment statements in Go (and most imperative languages) is to store a value in a variable somewhere in memory. Given an arbitrary assignment statement e1 = e2, this is always* compiled as if:

ptr, val := &e1, e2
store(ptr, val)  // *ptr = val

(Exception: map assignment logically and even internally works this way, though &m[k] isn't allowed in Go source programs.)

There's no "custom logic" to unpack *p = F(). It follows the form above and is compiled as if:

ptr, val := &*p, F()
store(ptr, val)  // *ptr = val

You're proposing to add uniquely new syntax and semantics for how = operates. That's a high bar, and it needs to be justified with sufficient examples of real world Go code that would benefit from it.

Typically a multi-valued functions that return a pointer (i.e., the functions where this proposal are applicable to) is going to have a return signature like (*T, error). Moreover, it's the callers responsibility to check the error value before dereferencing the pointer. It would be counter-productive to make it easier for users to dereference the *T pointer before they can check for errors.

Notably, your initial example (&s.URL, err = url.Parse(s); return err) suffers from exactly this problem. It dereferences the returned *url.URL value before checking whether err is nil.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Aug 25, 2020

Based on the discussion above, this is a likely decline. Leaving open for four weeks for final comments.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 30, 2020

No further comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
7 participants
You can’t perform that action at this time.