golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.35k stars 17.71k forks source link

proposal: spec: permit referring to a field shared by all elements of a type set #48522

Closed beoran closed 1 year ago

beoran commented 3 years ago

What version of Go are you using (go version)?

$ go version
/tmp/golang-tip/bin/go version                                                                                                                      
go version devel go1.18-986f8ea6b4 Tue Sep 21 00:59:42 2021 +0000 linux/amd64

Does this issue reproduce with the latest release?

No, it is a generics issue, therefore tested with a recent tip only.

What operating system and processor architecture are you using (go env)?

linux/amd64


go env Output
$ go env
GO111MODULE="" 
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/me/.cache/go-build"
GOENV="/home/me/.config/go/env"            
GOEXE=""                                                                                                                                                                             GOEXPERIMENT=""                                                                                                                                                                      GOFLAGS=""                                                                                                                                                                           GOHOSTARCH="amd64"                                                                                                                                                                   GOHOSTOS="linux"                                                                                                                                                                     GOINSECURE=""                                                                                                                                                                        GOMODCACHE="/home/me/src/go/pkg/mod"                                                                                                                                           GONOPROXY="k8s.io/*"
GONOSUMDB=""                                                                                                                                                                         GOOS="linux"                                                                                                                                                                         GOPATH="/home/me/src/go"                                                                                                                                                       GOPRIVATE=""                                                                                                                                                                         GOPROXY=""                                                                                                                                 
GOROOT="/tmp/golang-tip"                                                                                                                                                             GOSUMDB="off"                                                                                                                                                                        GOTMPDIR=""                                                                                                                                                                          GOTOOLDIR="/tmp/golang-tip/pkg/tool/linux_amd64"                                                                                                                                     GOVCS=""                                                                                                                                                                             GOVERSION="devel go1.18-986f8ea6b4 Tue Sep 21 00:59:42 2021 +0000"                                                                                                                   GCCGO="gccgo" 
GOAMD64="v1"                                                                                                                                                                         AR="ar"                                                                                                                                                                              
CC="gcc"                                                                                                                                                                             CXX="g++"                                                                                                                                                                            CGO_ENABLED="1"                                                                                                                                                                      GOMOD="/home/me/src/gocrtp/go.mod"                                                                                                                                             CGO_CFLAGS="-g -O2"                                                                                                                                                                  CGO_CPPFLAGS=""                                                                                                                                                                      CGO_CXXFLAGS="-g -O2"                                                                                                                                                                CGO_FFLAGS="-g -O2"                                                                                                                                                                  CGO_LDFLAGS="-g -O2"                                                                                                                                                                 PKG_CONFIG="pkg-config"                                                                                                                                                              GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1474680903=/tmp/go-build -gno-record-gcc-switches"

What did you do?

I tried to compile this program (crtp.go) with generics:

package main

import "fmt"

type Point struct {
    X, Y int
}

type Rect struct {
    X, Y, W, H int
}

type Elli struct {
    X, Y, W, H int
}

func GetX[P interface { Point | Rect | Elli }] (p P) int {
    return p.X
}

func main() {
    p := Point { 1, 2}
    r := Rect {2, 3, 7, 8}
    e := Elli {4, 5, 9, 10}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}

with tmp/golang-tip/bin/go build

What did you expect to see?

Program compiles, runs and outputs X: 1 2 4

What did you see instead?

./crtp.go:19:11: p.X undefined (type bound for P has no method X)

All three structs in the type bound have an identical X /field/, so I think this is wrong. Of course there is no method but I don't think that matters here. I feel I should be able to use the public field X of p since p can only be one of the three Point, Rect, or Elli.

beoran commented 3 years ago

@gopherbot, please add label generics

dr2chase commented 3 years ago

@griesemer

Reading "Composite types in constraints" in https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md I get the impression that this ought to work; the example there says "this doesn't work because the field types for X don't all match" which implies that if the field types did match, as they do here, then it would work.

package main

import "fmt"

type hasX interface {
    struct {
        X, Y int
    } | struct {
        X, Y, H int
    } | struct {
        X, Y, W, H int
    }
}

func GetX[P hasX](p *P) int {
    return p.X
}

func main() {
    p := struct {
        X, Y int
    }{1, 2}
    r := struct {
        X, Y, H int
    }{2, 3, 8}
    e := struct {
        X, Y, W, H int
    }{4, 5, 9, 10}
    fmt.Printf("X: %d %d %d\n", GetX(&p), GetX(&r), GetX(&e))
}
griesemer commented 3 years ago

We don't currently support field accesses of this kind even though the proposal says that this should/could work. We may not support this for Go1.18 as it doesn't seem like an essential feature. There's a trivial work-around that uses a method:

package main

import "fmt"

type Point struct {
    X, Y int
}

func (p Point) GetX() int { return p.X }

type Rect struct {
    X, Y, W, H int
}

func (r Rect) GetX() int { return r.X }

type Elli struct {
    X, Y, W, H int
}

func (e Elli) GetX() int { return e.X }

func GetX[P interface { Point | Rect | Elli; GetX() int }] (p P) int {
    return p.GetX()
}

func main() {
    p := Point { 1, 2}
    r := Rect {2, 3, 7, 8}
    e := Elli {4, 5, 9, 10}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}

Of course, then you don't need generic code in the first place because you could just use dynamic method dispatch. And maybe you should.

beoran commented 3 years ago

The reason I tried this out is for #48499, wherr the OP directly wants to get fields or pointers to fields from a set of similar structs. In that use case, the overhead of an accessor function and dynamic method dispatch would be not acceptable.

So while it may not be essential, #48499, which is for use with Go language databases, goes to show that it is not academic, but actually would be very useful for existing code, in stead of the feature proposed in that issue. Furthermore it is more consistent and easier to learn to also allow it.

If there is not enough time left to implement this for 1.18, then please consider this for 1.19.

Canadadry commented 2 years ago

Open question here.

I feel this is a bit restrictive because we have to explicitly say which struct statisfy the constraint. Like for the interface we may want something that will allow us to tell what is the constraint not who can pass. For example :

package main

import "fmt"

type Point struct {
    X, Y int
}

type Rect struct {
    X, Y, W, H int
}

type Elli struct {
    X, Y, W, H int
}

func GetX[P struct { X int }] (p P) int {
        // here p is known to have only the field X anything more
    return p.X
}

func main() {
    p := Point { 1, 2}
    r := Rect {2, 3, 7, 8}
    e := Elli {4, 5, 9, 10}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}

With this example we can pass any struct that has an int field name X, our function is not coupled with some defined type.

ghostsquad commented 2 years ago

func GetX[P struct { X int }] (p P) int {

possibly another way to describe this is..

type Xer struct {
   X int
}

func GetX[P ~Xer](p P) int {
    return p.X
}

Just stopping by to say that this is exactly something that I was looking for. Basically, how can I avoid explicitly defining the accessors (via an interface), especially in the case when I fully intend on allowing both Get & Set to occur.

I feel that gymnastics are a totally reasonable expectation when I either want only 1 of Get/Set, OR if the Get/Set needs extra explicit handling. In that case, an interface makes sense. Otherwise, it's just boilerplate.

davidmdm commented 2 years ago

I believe that this kind of generic structural typing would be extremely useful, and necessary for systems that have performance requirements that dynamic dispatch cannot handle.

That being said the above suggestions for type sets with struct members only being structural seems to be against the grain of the type set. If I were to write the follow:

type Named interface {
  string |  struct { Name string }
}

I expect the set of types to be the set that consists of string and the struct literal that Has a Name property of type string. Not the set of string and any struct with property Name. Adding the ~ operator does help express perhaps that we want structural typing for the struct, but it is inconsistent with what came before in 1.18 where the meaning of the ~ operator is to mean any type's who's underlying type is what follows the operator.

Therefore, and this might be extremely unpopular, would it not make more sense to extend the constraint/interface syntax to allow for struct members?

type Xer interface {
  X int
}

In the same way that type XGetter interface{ GetX() int } represents the set of types that implement the method GetX, Xer would be the set of types that have an accessor X ?

This way we don't need to touch what the tilde operator means, or how to interpret type unions? Otherwise I think we would be taking two steps backwards in regards to eventually supporting sum types (if ever).

EDIT:

To add a more concrete scenario suppose:

type Point1D { X int }
type Point2D { X, Y int }
type Point3D { X, Y, Z int }

// type constraint
type TwoDimensional interface { X, Y int }

// Works for any struct with X and Y of type int, including Point2D and Point3D, etc.
func SomePlanar2DOperation[T TwoDimensional](value T) { ... }
beoran commented 2 years ago

Many interesting ideas here. But, if the use case of my original examples could work somehow, I don't mind the implementation details.

davidmdm commented 2 years ago

I think my idea might need to be rewritten as a proposal. As far as whether common fields of a struct union should usable from the function body, I agree. But somebody who knows if it is implementable should comment.

yulrizka commented 2 years ago

I have a real scenario where I'm working on ETL jobs that work nicely with generics. It removes the necessity to do type checking with switch.

The only thing that prevents it to work is assigning a value of a Field to the struct. And since there is a lot of struct object and each has a lot of properties, adding setter / getter for each struct seems to defeat the whole purpose.

zigo101 commented 2 years ago

It is strange that if all the structs have the same underlying type, then the code works.

package main

import "fmt"

type Point struct {
    X, Y, W, H int
}

type Rect struct {
    X, Y, W, H int
}

type Elli struct {
    X, Y, W, H int
}

func GetX[P interface { Point | Rect | Elli }] (p P) int {
    return p.X
}

func main() {
    p := Point { 1, 2, 0, 0}
    r := Rect {2, 3, 7, 8}
    e := Elli {4, 5, 9, 10}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}

However, I didn't find the ~ sign in the code.

[Edit], maybe the reason why it works is the constraint has a core type: https://tip.golang.org/ref/spec#Core_types

zigo101 commented 2 years ago

Promoted fields also work (if their underlying types are the same):

package main

import "fmt"

type Base struct {
    X, Y, W, H int
}

type Point struct {
    Base
}

type Rect struct {
    Base
}

type Elli struct {
    Base
}

func GetX[P interface { Point | Rect | Elli }] (p P) int {
    return p.X // okay
}

func main() {
    p := Point {}
    r := Rect {}
    e := Elli {}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}

But hybrid promoted and direct fields don't work:

package main

import "fmt"

type Point struct {
    X, Y, W, H int
}

type Rect struct {
    Point
}

type Elli struct {
    Point
}

func GetX[P interface { Point | Rect | Elli }] (p P) int {
    return p.X // error: p.X undefined (type P has no field or method X)
}

func main() {
    p := Point { 1, 2, 0, 0}
    r := Rect {}
    e := Elli {}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}
zigo101 commented 2 years ago

Different but with some similarity:

package main

func bar[F func(int) | func(int)int] (f F) {
    f(1) // invalid operation: cannot call non-function f (variable of type F constrained by func(int)|func(int) int)
}

func main() {}
blackgreen100 commented 2 years ago

@griesemer

We don't currently support field accesses of this kind even though the proposal says that this should/could work.

It doesn't support method access either.

Playground: https://gotipplay.golang.org/p/aEwSDTXYOlL

Of course method access can be solved by adding the common method to the constraint:

type AB interface {
    *A | *B
    Foo() bool
}

I would like to ask

  1. does method access not work for the same reason as field access?
  2. Is there a more formal explanation for why this doesn't work beside "it wasn't implemented"? It'd be useful when explaining this issue to others. It seems to me that the only relevant quote in the Go 1.18 specifications is "A type constraint [...] controls the operations supported by values of that type parameter." and then adding that selector expressions are not operations. Is there anything else to go by?

EDIT: Unlike for field access, the type parameter proposal seemed to be much more explicit about this Method sets of constraint elements:

The method set of a union element is the intersection of the method sets of the elements of the union. These rules are implied by the definition of type sets, but they are not needed for understanding the behavior of constraints.

This quote has then been ported to the Go 1.18 specs:

The method set of an interface type is the intersection of the method sets of each type in the interface's type set (the resulting method set is usually just the set of declared methods in the interface).

Emphasis on usually, which doesn't exclude pushing into the interface's method set the intersection of the methods of the type terms. So... on a second thought, method access not working seems a bug (maybe a documentation bug). Am I misreading something?

zigo101 commented 2 years ago

@blackgreen100 That is https://github.com/golang/go/issues/51183

griesemer commented 2 years ago

Is there a more formal explanation for why this doesn't work beside "it wasn't implemented"? It'd be useful when explaining this issue to others. It seems to me that the only relevant quote in the Go 1.18 specifications is "A type constraint [...] controls the operations supported by values of that type parameter." and then adding that selector expressions are not operations. Is there anything else to go by?

The Go 1.18 release is a huge release and we simply haven't had the bandwidth to get everything done.

ianlancetaylor commented 2 years ago

@blackgreen100 Note that the method set limitation is explicitly called out in the release notes (https://go.dev/doc/go1.18#generics).

mdempsky commented 2 years ago

This isn't going to happen in Go 1.19.

CarsonSlovoka commented 2 years ago

A little disappointed, but as long as the request is not rejected, it is still good.

jamestrandung commented 2 years ago

Sorry for a question that is not related to this topic. Can someone please point me to a documentation for that ~ operator? I tried to google but couldn't find anything. I wonder how it works.

type Xer struct {
   X int
}

func GetX[P ~Xer](p P) int {
    return p.X
}
ZekeLu commented 2 years ago

The type set of a term of the form ~T is the set of types whose underlying type is T.

See https://go.dev/ref/spec#General_interfaces

elbek commented 2 years ago

I fee like without support for what was proposed in original post go generics simply incomplete. With few fields maybe we could create getter methods and do the work, but this is very cumbersome and kills the purpose of generics say struct has 10-20 fields and it is not uncommon to have this many.

ianlancetaylor commented 2 years ago

We agree that the support for generics is not yet complete. We're working on it actively. This issue remains open.

That said, are there cases where generics are helpful with structs of 10 to 20 fields? This feature is only useful if you have multiple structs with that many fields, where the fields you care about have identical names and types, and it's useful to write a complex constraint that is a union of those different struct types. When does this case arise in practice?

tkinz27 commented 2 years ago

I typically have a lot of database models with a consistent set of attributes on them (updated_at, updated_by, created_by, created_at, for example). We have about 7 common attributes (and potentially growing, just talked about adding a some location based attributes) we use for authorization on all our resources and for a generic authz check on a model it be nice to pass in any type as long as it had those attributes. We define our models in proto so while its possible to add a getter method and use an interface, its fairly error prone to ensure every developer remembers to add that method after the go code is generated.

ianlancetaylor commented 2 years ago

It's important to be clear on what is being suggested here. This is not "pass in any type as long as it had those attributes." This is "pass in one of this specific list of types, each of which has these attributes, and then refer to those attributes in the generic code."

That is, code like this would be permitted.

type S1 struct { F, G int }
type S2 struct { F, H int }
type C interface { S1 | S2 }
func GetF[T C](s T) int { return s.F }
var x = GetF(S1{})

Code like this would not be permitted.

type S struct { F int }
type S1 struct { F, G int }
type S2 struct { F, H int }
type C interface { S }
func GetF[T C](s T) int { return s.F }
var x = GetF(S1{}) // INVALID: S1 does not satisfy constraint C
beoran commented 2 years ago

At work I have some complex data that consist of similar union types stored in the same database table, which have many common fields. For those, this feature would certainly help to implement a closed union type using generics.

zigo101 commented 2 years ago

@tkinz27 Maybe what you need is described in https://github.com/golang/go/issues/51259.

tkinz27 commented 2 years ago

@ianlancetaylor Thanks for clarifying, I misunderstood.

@go101 yes that is much closer to what I was thinking.

beoran commented 2 years ago

Personally I am more in favor of this issue than #51259, because this does not introduce any new syntax, but only allows semantics that are currently not allowed. This issue would still be very useful for handling the common data of closed unions and other cases of a known set of structs with common fields.

egtann commented 2 years ago

This feature is only useful if you have multiple structs with that many fields, where the fields you care about have identical names and types, and it's useful to write a complex constraint that is a union of those different struct types. When does this case arise in practice?

@ianlancetaylor We have a DAG with many nodes at my company that forms the core of the product we sell. Each node is a struct which contains the same fields ID and ParentID, as well as many other fields unique to each node type. There are also a subset of nodes which have an overlapping 12 additional fields -- 14 in total.

There are 56 different types of nodes. We've needed to implement an interface with getters and setters for each node. It's easy, but it's also tedious to write and maintain and leads to arguably less conventional code in use, since now we need to do GetID() where .ID would be preferred. The file that contains these getter and setter definitions for the nodes is almost 3000 lines long.

benitogf commented 1 year ago

This feature is only useful if you have multiple structs with that many fields, where the fields you care about have identical names and types, and it's useful to write a complex constraint that is a union of those different struct types. When does this case arise in practice?

I wanted to make a generic event queue with the constraint on the provided struct to have a duration and id field

type Event struct {
    ID       string
    Duration int64
        ....
}
randall77 commented 1 year ago

Whatever happens here, will not happen for 1.20. Bumping to 1.21.

diamondhands0 commented 1 year ago

Just wanted to add one more anecdote to this thread.

We have a few dozen types that all have a common field isDeleted, like you can see here.

A common check we do is if entry != nil && !entry.isDeleted {.

We wanted to simplify this check into a simple IsEntryDeleted(entry), which would be much less error-prone. But we couldn't because the following code requires accessing a member :(

// A generic utility function to check whether an Entry has been deleted from the view.
func IsEntryDeleted[E any](entry *E) bool {
    if entry == nil || entry.isDeleted {
        return true
    }
    return false
}

Of course, we could define an interface with a common getter for every single type that we have. But this would be highly redundant at this point, and the compiler should clearly be smart enough to pick up on this member being common across all types.

I imagine many such use-cases exist where someone wants to do a complicated check on a struct's members, with multiple structs all having the same common members. Libraries that deal with vector math come to mind, where there may be multiple different structs that rely on a common "core" of members that require similar operations to be run on them.

It does also ultimately feel like it would be more "elegant" if [E any] were to allow one to use the E type exactly as if the struct's actual type were substituted in its place.

Anyway, just wanted to say that it would be amazing if struct members could be accessible via generic functions in 1.21 :)

beoran commented 1 year ago

@griesemer Looking at this thread, there seems to be a wide consensus amongst the people in this thread that implementing this feature is a good idea. However it has been postponed several times now. It would be great if this was accepted, so this could be implemented for 1.21

ziposcar commented 1 year ago

I found a description in the go1.18 release note https://go.dev/doc/go1.18 : The Go compiler does not support accessing a struct field x.f where x is of type parameter type even if all types in the type parameter's type set have a field f. We may remove this restriction in a future release. I'm looking forward to this future release coming soon.

crazyoptimist commented 1 year ago

We may remove this restriction in a future release.

+1

snutiise commented 1 year ago

I really hope this issue is resolved

subtle-byte commented 1 year ago

btw Original example can be fixed using embedded fields:

Code ```go package main import "fmt" type xField int func (x xField) X() int { return int(x) } func (x *xField) SetX(newX int) { *x = xField(newX) } type yField int func (y yField) Y() int { return int(y) } func (y *yField) SetY(newY int) { *y = yField(newY) } type wField int func (w wField) W() int { return int(w) } func (w *wField) SetW(newW int) { *w = wField(newW) } type hField int func (h hField) H() int { return int(h) } func (h *hField) SetH(newH int) { *h = hField(newH) } type Point struct { xField yField } type Rect struct { xField yField wField hField } type Elli struct { xField yField wField hField } func GetX[P interface{ X() int }](p P) int { return p.X() } func main() { p := Point{1, 2} r := Rect{2, 3, 7, 8} r.SetW(8) e := Elli{4, 5, 9, 10} fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e)) } ```

It is not very beautiful, but can reduce the amount of code if you need to write getters/setters. But I think the syntax type withX interface {X int} would be much cleaner.

avivdolev commented 1 year ago

Just another example for a use case and another workaround, this time without an interface.

package options

import "io"

type GetOpts struct {
    url     string
    headers map[string]string
}

type PostOpts struct {
    url     string
    headers map[string]string
    body    io.Reader
}

type opts interface{ GetOpts | PostOpts }

// Desired implementation, for now it's a compile-time error
func WithHeader[Opts opts](o Opts, k, v string) Opts {
    if o.headers == nil {
        o.headers = make(map[string]string)
    }
    o.headers[k] = v
    return o
}

// With current generics, without getter/setter interfaces
// `headers` param is defined as generic only to make it easier to change
func withHeader[Opts opts, headers map[string]string](o Opts, k, v string,
    getter func(Opts) headers,
    setter func(*Opts, headers)) Opts {
    h := getter(o)
    if h == nil {
        h = make(headers)
    }
    h[k] = v
    setter(&o, h)
    return o
}
// Publishing a cleaner API usage without generics, that benefits from the single implementation
// Imagine a real-world use case where the verification is more complicated (e.g. appending a value to existing key instead of overriding)
func (o PostOpts) WithHeader(k, v string) PostOpts {
    return withHeader(o, k, v,
        func(o PostOpts) map[string]string { return o.headers },
        func(o *PostOpts, h map[string]string) { o.headers = h })
}

func (o GetOpts) WithHeader(k, v string) GetOpts {
    return withHeader(o, k, v,
        func(o GetOpts) map[string]string { return o.headers },
        func(o *GetOpts, h map[string]string) { o.headers = h })
}
griesemer commented 1 year ago

On the typechecker side, the 1.21 release focuses on (much better) type inference. Didn't get to this. Moving along to 1.22.

zaxiom13 commented 1 year ago

Completely new to go but here would be my use case for field access on generics:

type User struct {
    name string
    password string
}

type Admin struct {
    name string
    level string
    password string
}

func redactPassword[T User | Admin](someUser T) (T){
    someUser.password = "*****"
    return someUser
}

func main() {
    fmt.Println("hello world?")

    user := User{"apple","banana"};

    fmt.Println(redactPassword(user))

}

lets me use the same function wherever i need to log the details of a struct without exposing one of the fields

marmiha commented 1 year ago

Promoted fields also work (if their underlying types are the same):

package main

import "fmt"

type Base struct {
  X, Y, W, H int
}

type Point struct {
  Base
}

type Rect struct {
  Base
}

type Elli struct {
  Base
}

func GetX[P interface { Point | Rect | Elli }] (p P) int {
  return p.X // okay
}

func main() {
  p := Point {}
  r := Rect {}
  e := Elli {}
  fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
}

The above example does not work, or am I missing something obvious.

sirockin commented 1 year ago

@marmiha: The above doesn't work but you can work around this by creating an interface to get/set the common base fields then using the interface as the constraint for your generic:

package main

import "fmt"

type IBase interface {
    GetX() int
}

type Base struct {
    X, Y, W, H int
}

func (b Base) GetX() int {
    return b.X
}

type Point struct {
    Base
}

type Rect struct {
    Base
}

type Elli struct {
    Base
}

func GetX[T IBase](v T) int {
    return v.GetX()
}

func main() {
    p := Point{}
    r := Rect{}
    e := Elli{}
    fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))

}
piroux commented 1 year ago

Now that this is a Proposal, what are the items that need to be discussed to converge towards an Accepted Proposal? (hopefully)

ianlancetaylor commented 1 year ago

It needs to go through proposal review committee. See https://go.dev/s/proposal.

rsc commented 1 year ago

This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — rsc for the proposal review group

fzipp commented 1 year ago

My concern is that if Go keeps expanding the options for specifying type sets in constraints but doesn't do the same for plain old interface types, programmers might choose type parameters simply because they allow them to define more intricate type sets compared to regular interface types, rather than because the problem truly demands the use of type parameters.

// This is a problem for which type parameters are an appropriate choice.
func F1[T TypeSet](s []T) T

// This, on the other hand, is not.
func F2(x TypeSet)         
Merovius commented 1 year ago

@fzipp I don't believe this proposal actually is about "expanding the options for specifying type sets in constraints". This proposal is to write generic code you can already write, but do it more conveniently. That is, you can already write

type A struct { X int; Y string }
type B struct { X int; Y float64 }
type C interface{ A | B }
func F[T C](p *T) {
    switch p := any(p).(type) {
    case *A:
        p.X = 42
    case *B:
        p.X = 42
    }
}

This proposal is just about being able to instead write

func F[T C](p *T) {
    p.X = 42
}

(Perhaps you are mixing this up with #51259, which is indeed about expanding the options to specify constraints)

fzipp commented 1 year ago

@Merovius

What I mean is that the code in your comment could also be written like this:

type A struct { X int; Y string }
func (a *A) SetX(x int) { a.X = x }

type B struct { X int; Y float64 }
func (b *B) SetX(x int) { b.X = x }

type C interface{ SetX(x int) }

func F(p C) {
    p.SetX(42)
}

It's longer, and some people might be inclined to skip defining setter methods out of laziness, opting to write their code using a type parameter like the code in your comment. However, this choice doesn't stem from an inherent need for type parameters. Instead, it comes down to a preference for brevity, because we fail to provide the same brevity with regular interface types.

Merovius commented 1 year ago

That is not the same code - in particular, the types A and B now have different method sets. Also, there is no reason to assume that the package implementing F is the same as implementing A and B. And none of that changes the fact that you can already write the example I wrote and that this proposal is only about making it more convenient - and in particular, that it is not about expanding the constraints that it is possible to express.