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
urandom2 opened this issue Aug 12, 2020 · 19 comments
Closed

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

urandom2 opened this issue Aug 12, 2020 · 19 comments
Labels
Milestone

Comments

@urandom2
Copy link
Contributor

urandom2 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
@davecheney
Copy link
Contributor

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

@urandom2
Copy link
Contributor Author

urandom2 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

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

@urandom2
Copy link
Contributor Author

urandom2 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

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 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 ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Aug 12, 2020
@ianlancetaylor

This comment has been minimized.

@ianlancetaylor
Copy link
Contributor

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?

@urandom2
Copy link
Contributor Author

urandom2 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

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 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.

@urandom2
Copy link
Contributor Author

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.

@urandom2

This comment has been minimized.

@mdempsky
Copy link
Member

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?

@urandom2
Copy link
Contributor Author

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

@urandom2
Copy link
Contributor Author

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

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

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

@ianlancetaylor
Copy link
Contributor

No further comments.

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

No branches or pull requests

7 participants