Navigation Menu

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: Field expressions #59101

Closed
jwebb opened this issue Mar 17, 2023 · 17 comments
Closed

proposal: spec: Field expressions #59101

jwebb opened this issue Mar 17, 2023 · 17 comments
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Milestone

Comments

@jwebb
Copy link

jwebb commented Mar 17, 2023

Go has a concise notation to derive a function from a method, https://go.dev/ref/spec#Method_expressions, i.e. we have notional desugarings of the form

MyStructType.MyMethod    ==>  func(x MyStructType) ResultType { return x.MyMethod() }

The proposal is to allow the same for fields:

MyStructType.MyField     ==>  func(x MyStructType) FieldType { return x.MyField }
(*MyStructType).MyField  ==>  func(x *MyStructType) FieldType { return x.MyField }

This seems a simple and backward-compatible extension to the spec (the above forms are currently rejected). Compilation is likely slightly more complicated than for methods, as it would require emitting and deduplicating synthetic getters, but seems not unreasonable.

The use-case is code that is parameterized by the structure of the data it works on, which has become much more plausible with the introduction of generics. For example, given:

type Person struct {
	Name string
	Age  int
}

func SortByField[S any, T constraints.Ordered](slice []S, getter func(S) T) {
	slices.SortFunc(slice, func(a, b S) bool { return getter(a) < getter(b) })
}

var people = []Person{{"Bilbo", 111}, {"Frodo", 33}}

We can replace

SortByField(people, func(p Person) int { return p.Age })

with the more readable

SortByField(people, Person.Age)
@jwebb jwebb added the Proposal label Mar 17, 2023
@gopherbot gopherbot added this to the Proposal milestone Mar 17, 2023
@randall77
Copy link
Contributor

I think this might lead to confusion, as

type Person {
   Name string
}
func (p *Person) Foo() { }
var x Person

Then Person.Foo is a function taking a *Person, and Person.Name is (with your proposal) also a function taking a *Person.

But x.Foo is a func(), and x.Name is a string. It seems odd that the "static" versions correspond, but the "instantiated"/"curried"/whatever you call it versions do not.

@DeedleFake
Copy link

DeedleFake commented Mar 17, 2023

In a weird sort of way, this actually correlates with the way that method expressions and method values work, like it's filling in a missing slot in a 4x4 2x2 table. For example,

type E struct { V string }
func (E) M() string {}
var s E

s.M // func() string
E.M // func(E) string
s.V // string
E.V // Syntax error.

It is strange that it results in a function instead of a string, but I can't think of any other way to do it that would make sense.

I wonder if #21498 would be a more obvious and general way of solving this: SortByField(people, (p) => p.Age) is pretty simple, too.

Edit: Whoops. I can do math. Really.

@jwebb
Copy link
Author

jwebb commented Mar 17, 2023

Agree that this would be of limited value given => lambda support, but that is rightly a Go 2 proposal as it's a substantial syntax change. This proposal addresses what I see as a gap in the Go 1 spec, per your 4x42x2 characterisation.

@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Mar 17, 2023
@ianlancetaylor
Copy link
Contributor

Thanks for the suggestion. This is an interesting idea.

There is only one operation you do on a method: you can call it. But there are (at least) two operations you can do on a field: you can retrieve the value and you can set the value. This proposal gives a special syntax for retrieving the value, but not for setting a value. In other words, this syntax is a getter, but there is no similar syntax for a setter. It doesn't seem right to have getters without setters.

A method expression takes a method, which is something that is callable, and evaluates to a function, which is also something that is callable. So method expressions are similar to methods in some sense. But that is not true for these field expressions. This is similar to what @randall77 said earlier.

@jwebb
Copy link
Author

jwebb commented Apr 13, 2023

In other words, this syntax is a getter, but there is no similar syntax for a setter.

This is quite simply worked around by defining the transform to always operate on pointers:

MyStructType.MyField     ==>  func(x *MyStructType) *FieldType { return &x.MyField }

It does break some symmetry with method expressions though. I could imagine separate syntax for pointers, e.g. (*Person).&Age but that looks a bit line-noisy (and Person.&Age makes no sense).

@ianlancetaylor
Copy link
Contributor

If the field expression returns a pointer then the nice SortByField(people, Person.Age) expression no longer works.

@ianlancetaylor
Copy link
Contributor

It's an interesting proposal, and there is an analogy to method expressions. But it doesn't seem to quite work for setters, and it's unclear how often it is really useful in practice.

Therefore, this is a likely decline. Leaving open for three weeks for final comments.

@youta-t
Copy link

youta-t commented May 11, 2023

Would you like new postfixes and explicit getter/setter?

  • Getter Field expression MyStruct.Field?: works as func(s MyStruct) FieldType { return s.Field }
  • Setter Field expression (*MyStruct).Field!: works as func(s *MyStruct, v FieldType) { s.Field = v }

(S.Field! may have no effect, even if it is allowed.)

The postfixes introduces both getter and setter, not only one.

Usage example:

type S struct {
    F FieldType
}

func MapS[T any](ss []S, f func(S)T) []T {
    ret := make([]T, len(ss))
    for i := range ss {
        ret[i] = f(ss[i])
    }
    return ret
}

func ZipIntoS[T any](ss []*S, values []T, f func(*S, T)) {
    for i := range ss {
        f(ss[i], value[i])
    }
}

var fields []string = MapS(
    []S{ {Field: "foo"}, {Field: "bar"}, {Field: "baz"} },
    S.F?,
)  // ==> []string{"foo", "bar", "baz"}

var sSlice = []*S{ {Field: "foo"}, {Field: "bar"}, {Field: "baz"} }
BroadcastS(
    sSlice, []string{"one", "two", "three"},
    (*S).F!,
)

sSlice // ==> []*S{ {Field: "one"}, {Field: "two"}, {Field: "three"} }

? and ! may be a pair of good postfixes.

  • backward compatible: Fields' name can not have ? or !.
  • no ambiguity: ? is not used as (a part of) operators / ! is used, but not as postfix.
  • concise: no more shorter postfixes than one character.

@ianlancetaylor
Copy link
Contributor

@youta-t That does not seem very Go-like to me.

@empire
Copy link
Contributor

empire commented May 11, 2023

If the field expression returns a pointer then the nice SortByField(people, Person.Age) expression no longer works.

I think the mentioned issue can be solved if we use * operator to dereference it if its Age is pointer and we can use & operator to pass it as pointer.

We can write:

type Person struct {
   Age *int
}

SortByField(people, Person.*Age) //
SortByField(people, func(p Person) int { return p.*Age })
type Person struct {
   Age int
}
...
SortByField(people, Person.&Age) // SortByField(people, func(p Person) *int { return &p.Age })

@empire
Copy link
Contributor

empire commented May 11, 2023

But it doesn't seem to quite work for setters, ...

To make it work on setters, I propose the following solution with one restriction.
The restriction is that it is not allowed to pass the Field to an unspecified function type and only allowed to assign it to func(t T) R or func(t T, a A). So when it is assigned to SortByField[S []T, T any, R any](s S, func(t T) R), it will be used as a getter form, and when it is assigned to SetByField[S []T, T any, R any](s S, func(t T, r R)), the setter will be used.
The other cases are invalid and not allowed.
For example:

// valid case: using getter form
SortByField(people, Person.Age)

// valid case: using setter form
SetByField(people, Person.Age)

// invalid case: passing Field to an unspecified type

var f = Person.Age

@ianlancetaylor
Copy link
Contributor

The restriction is that it is not allowed to pass the Field to an unspecified function type and only allowed to assign it to func(t T) R or func(t T, a A).

Thanks, but we aren't going to do that. Nothing else in Go behaves that way.

@empire
Copy link
Contributor

empire commented May 12, 2023

Thanks, but we aren't going to do that. Nothing else in Go behaves that way.

I respect the decision as a whole, but I'm curious about something. Is it similar to what the runtime does to implement map indexing operations?

@youta-t
Copy link

youta-t commented May 12, 2023

I posted draft and it is deleted. Sorry.
I've wrote that now! Sorry again.


@youta-t That does not seem very Go-like to me.

Ah, yes, it is introducing something new to avoid breaking existing codes. The weirdness is intended, to a certain degree.

Considering Go-likeness, "Field Expression as Accessor" might be alternative.

Suppose that we have Accessor[S, F], abstraction of field-access, like below.

type Accessor[S, F any] interface {
    Gettable[S, F]
    Settable[S, F]
}

type Gettable[S, F any] interface {
    // Get a field value typed F from S
    Get(S)F
}

type Settable[S, F any] interface {
    // Set a field typed F in S
    Set(S, F)
}

For type S { F TF }, field expression S.F is typed as Accessor[S, TF], and we can access the field with that.

Using this, we can write like next:

type SomeStruct struct {
    Field string
}

var sAccessor Accessor[SomeStruct, string] = SomeStruct.Field
var _ func(S)F   = sAccessor.Get   // = func(s S) string { return s.Field }
var _ func(S, F) = sAccessor.Set   // = func(s S, f string) { s.Field = f }, but may have no effect.

var pAccessor Accessor[*SomeStruct, string] = (*SomeStruct).Field
var _ func(*S)F   = pAccessor.Get  // = func(s *S) string { return s.Field }
var _ func(*S, F) = pAccessor.Set  // = func(s *S, f string) { s.Field = f }

var fields []string = MapS(
    []SomeStruct{ {Field: "foo"}, {Field: "bar"}, {Field: "baz"} },
    SomeStruct.Field.Get,
)  // ==> []string{"foo", "bar", "baz"}

var sSlice = []*SomeStruct{ {Field: "foo"}, {Field: "bar"}, {Field: "baz"} }
BroadcastS(
    sSlice, []string{"one", "two", "three"},
    (*SomeStruct).Field.Set,
)
sSlice // ==> []*S{ {Field: "one"}, {Field: "two"}, {Field: "three"} }

Accessor is in golang syntax. No wierd (isn't it?).

It is a bit (or 3 bytes) longer than postfixes, but concise enough for me.

@ianlancetaylor
Copy link
Contributor

@empire

Is it similar to what the runtime does to implement map indexing operations?

I'm not sure I understand the question. Map index expressions are like variable references; they act differently depend on whether they are on the left or right hand side of =. Functions do not change in that way.

@ghost
Copy link

ghost commented Jun 5, 2023

I agree with Ian, this isn't really needed, and would be a maintenance burden. just write a callback.

@ianlancetaylor
Copy link
Contributor

There was further discussion, but no change in consensus. Closing.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Jun 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Proposal Proposal-FinalCommentPeriod v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants