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: permit referring to a field shared by all elements of a type set #48522

Closed
beoran opened this issue Sep 21, 2021 · 85 comments
Closed
Assignees
Labels
generics Issue is related to generics Proposal
Milestone

Comments

@beoran
Copy link

beoran commented Sep 21, 2021

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
Copy link
Author

beoran commented Sep 21, 2021

@gopherbot, please add label generics

@gopherbot gopherbot added the generics Issue is related to generics label Sep 21, 2021
@dr2chase dr2chase added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Sep 21, 2021
@dr2chase
Copy link
Contributor

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

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
Copy link
Author

beoran commented Sep 21, 2021

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.

@griesemer griesemer added NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Nov 17, 2021
@griesemer griesemer added this to the Go1.19 milestone Nov 17, 2021
@Canadadry
Copy link

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
Copy link

ghostsquad commented Feb 9, 2022

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
Copy link

davidmdm commented Feb 16, 2022

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
Copy link
Author

beoran commented Feb 17, 2022

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

@davidmdm
Copy link

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
Copy link

yulrizka commented Feb 18, 2022

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.

@go101
Copy link

go101 commented Feb 24, 2022

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

@go101
Copy link

go101 commented Feb 24, 2022

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))
}

@go101
Copy link

go101 commented Feb 25, 2022

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
Copy link

blackgreen100 commented Mar 7, 2022

@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?

@go101
Copy link

go101 commented Mar 7, 2022

@blackgreen100 That is #51183

@griesemer
Copy link
Contributor

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
Copy link
Contributor

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

@mdempsky
Copy link
Member

This isn't going to happen in Go 1.19.

@griesemer
Copy link
Contributor

The intent of the original generics proposal was that an operation on a type parameter (i.e., on a value of type parameter type) shall be permitted if the operation is permitted on each element of the type set defined by the type parameter's constraint (see Operations based on type sets).

This issue is simply proposing that we apply this principle also to struct field accesses. So this proposal is in line with the original vision for generics, and essentially removes an implementation restriction.

For comparison, I note that it's currently possible to access a slice element of a generic slice type, if all elements in the respective type set allow the slice operation. This code is valid (playground):

type S1 []int

func get[S S1 | []int](s S, i int) int {
	return s[i]
}

(Here the type set of S contains just two types, the S1 and []int type.)

That said, this slices example is a slightly bit different from the proposal at hand because the type set of S has a core type, and the index access is based on that core type. We have used the mechanism of the core type to explain behavior where we don't have a "traditional" unary or binary operator such as +, *, etc. Field selection (the . "dot" operation) would fall more in the latter category where the rule is not based on a core type but on the fact that all type set elements support the operation.

@phenpessoa
Copy link

If this is accepted, will the following work?

type A struct {
	Foo int
	Bar string
}

type B struct{ Foo int }

type C interface{ A | B }

func foo[T C](f T) int { return f.Foo }

Isn't this too expensive for the compiler? To have to check every field of every possible type that satifies C? What if C has a lot of types and every type has a lot of fields?

@griesemer
Copy link
Contributor

I'm not sure what you mean by "expensive". The compiler already needs to check that each type argument to foo satisfies the C constraint (so test the type argument against each type in C). When compiling f.Foo, the compiler simply has see if f.Foo is valid for each possible type in C. We already do this all the time for operations such as +, -, etc. A field lookup is more expensive, but nor prohibitively so. And there's ways to cache information if so desired.

@phenpessoa
Copy link

By "expensive", I meant it would be a quadratic operation.

In the example I sent, currentely, the compiler only needs to check that the argument type passed is either A or B. It doesn't need to check what fields A or B have, I assume.

That's where my worry came from. But glad it isn't too expensive. Thanks!

@Merovius
Copy link
Contributor

Merovius commented Oct 9, 2023

@go101 The comment you link to is not an actual response. For one, you suggest as a solution to "define < for interfaces, like we do ==", which is a bad idea in and off itself - many algorithms (like sorting) assume that a < b || a == b || a > b is true. Putting a foot-gun in, where a < b might be false purely because their dynamic types don't match seems bad. But also, you move the goal-post - the actual example was a+b which definitely does not have a reasonable definition, if the dynamic types of a and b are different.

Either way - the point is that your assumption that any interface will definitely be a valid type argument for a type-parameter constrained on it is anything but given. From what I can tell, you are pretty much alone in the belief that will be the case. And in any case - selector-expressions won't make that any harder. We already have to overcome significant practical hurdles to make that happen (as illustrated with +) and selector-expressions would not be qualitatively different.

@phenpessoa
Copy link

Will there be a way to express a constraint as Any struct that has X, Y, Z field(s)? Not sure if this would be a different proposal.

For example:

type C interface{ ~struct{ Foo int } }

Currently, this require a struct that has exactly 1 field called Foo and that it's type is int. What I'm askig is that the constraint would allow for any struct that has the required field, so any struct with field Foo int would be allowed, even if it has more fields.

@Merovius
Copy link
Contributor

Merovius commented Oct 9, 2023

@phenpessoa That's #51259

@go101
Copy link

go101 commented Oct 10, 2023

@Merovius

many algorithms (like sorting) assume that a < b || a == b || a > b is true

This even can't be promised at today and the age before Go custom generics was introduced.

It looks you are over-confident on your opinion.

@jimmyfrasche
Copy link
Member

I prefer #51259. Allowing operations shared by all primitive types feels different. The intersection of the method sets is not included and the "field set" is more like that than indexing or addition or the like.

@seancfoley
Copy link

seancfoley commented Oct 11, 2023

Since this requests something that can already be done, and since methods contain code, which can differ amongst methods of the same signature, allowing the behaviour amongst methods is clearly more powerful and useful, while allowing this behaviour amongst fields simply clutters up the language, and invites pathological cases.

It gets really pathological when you have:

type Point struct {
	X, Y int
}

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

type MyRect struct {
	X, Y, W, H int32
}

type myint int64 

type MyOtherRect struct {
	X, Y, W, H myint
}

Using methods, this is easily handled. But with fields, this can get utterly ridiculous. Fields hold data. What type you use to hold that data should not matter. But now you wish to make it extremely important that you choose the same type as others, regardless of whether it really matters or not.

Seriously, the language already provides a straightforward and flexible way of accessing the fields above in a unified manner, by simply requesting that you specify the necessary code to access those fields, you simply tell everyone that you need a GetX() int and all the types above can easily comply. You can make the code do what it needs to do to provide your GetX() int. If you cannot comply, then accessing the fields directly also does not work, the different data types really matter and so you cannot get everyone to agree up on the correct type for 'X'.

Extending this to fields adds no value and makes the language more complicated. Yes, in rare cases you can avoid specifying that GetX() int method, but so what, you save a measly 3 lines of code to do so? Seriously?

With methods, it gives you the opportunity to decide exactly what GetX should return to make everyone happy. With fields, it may be impossible to coordinate across all developers whether they should choose uint, int, or something else.

If people really want to provide a unified and shared manner of access the "x" axis data point above, then asking them to provide a unified method of the same signature is much less of a burden than asking them to coordinate the field name and the data type to match exactly what other people have chosen.

Can you imagine what will happen in most cases? Half of developers will chose int, half will chose uint, and this feature will be completely useless.

It might be useful for a single developer, but as soon as code is shared it becomes useless.

@ziposcar
Copy link

I found a lot of comments proposing to use getters and setters. They do work. But unfortunately sometimes third-party packages refuse to provide setters. For example, the proto-gen-go of Protobuf. There are two closed proposals to add setters to fields:

golang/protobuf#65
golang/protobuf#664

As a user of this package, I felt helpless..
However, if this proposal is passed, some code generators will no longer require getters & setters.

@Merovius
Copy link
Contributor

Merovius commented Oct 11, 2023

@seancfoley I'm not sure what you are trying to demonstrate with your example. I don't think this proposal would allow to use selector-expressions with a union of those types either - obviously, a selector expression can only be allowed if all types in the type set have a common field with a common type.

Also, I don't find your general argument convincing. This proposal is about accessing a field that is common to all types in a given union. The author of that generic function knows what types are allowed and knows whether or not they share a common field.

You are correct that the GetX function in the top-post would not work with any of the types from your list. But - of course it does not. It only works with the types explicitly mentioned in its constraint. This proposal is not about a way to express "any type with a field X" as a constraint (that's #51259) it's about a situation where you know that a certain, limited set of types have a field in common.

@seancfoley
Copy link

seancfoley commented Oct 11, 2023

@Merovius My point is about the usefulness of a feature that would require similar data in different structs to synchronize on common field types.

Sure, the author knows whether he can do it or not, so what? That doesn't mean it's a worthwhile addition to the language. It's not. The feature becomes useless in a rather trivial manner.

It adds complexity to the language (something go has tried to avoid) for a benefit that is near zero.

@mitar
Copy link
Contributor

mitar commented Oct 20, 2023

I am not sure if this is already covered with this proposal, but I have another use case I haven't yet seen mentioned: providing default struct with functions which accept it, but user can pass another struct to them which have the default one embedded. So users can attach additional data and methods, but functions I provide do not care about them, but should pass the value through.

Example:

type Base struct {
	Name string
}

type Extended struct {
	Base
	Age int
}

func SayHi[T Base](x T) (T, string) {
	return x, fmt.Sprintf("Hi, %s!", x.Name)
}

func main() {
	x, hi := SayHi(Extended{Name: "foo", Age: 30})
	fmt.Println(hi)
	fmt.Println(x.Age)
}

The example is a bit contrived, but the idea is that SayHi should be able to access everything on embedded Base (both fields and methods), but the returned x is in fact of the type passed in. So the extra fields and methods are available and can be passed through, while my code just cares about the Base.

@zephyrtronium
Copy link
Contributor

@mitar That is #51259.

@go101
Copy link

go101 commented Oct 21, 2023

@mitar's example should not work. Because the type set of interface{Base} only contains one type: Base. So Extended is an invalid type argument. Besides this, Extended{Name: "foo", Age: 30} is an invalid literal.

@rsc
Copy link
Contributor

rsc commented Nov 1, 2023

@griesemer has been looking into possible ways to simplify the definition of generic type checking and in particular removing the concept of core types. It seems like we should wait on that effort before making any changes along these lines. This change might fall out of that, or it might not, but better to have a general cleanup than a pointwise fix that may or may not improve things.

In the interim there is always the ability to add explicit get/set methods and adding those to the constraint.

@rsc
Copy link
Contributor

rsc commented Nov 2, 2023

Based on the discussion above, this proposal seems like a likely decline.
— rsc for the proposal review group

@deefdragon
Copy link

I request this be put on hold instead of be declined if we are explicitly waiting on @griesemer's work. It doesn't make sense to me to decline on just the potential of it coming out of another solution.

@beoran
Copy link
Author

beoran commented Nov 2, 2023 via email

@piroux
Copy link

piroux commented Nov 3, 2023

@griesemer has been looking into possible ways to simplify the definition of generic type checking and in particular removing the concept of core types.

@griesemer
Which GH issues can we follow to watch the progress of this current research?

@griesemer
Copy link
Contributor

@piroux There wasn't one, but I created an umrella issue just now: #63940.

@rsc
Copy link
Contributor

rsc commented Nov 10, 2023

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

@rsc
Copy link
Contributor

rsc commented Nov 14, 2023

The bot did the wrong thing before. This was supposed to be declined, in favor of #63940.

@rsc rsc closed this as completed Nov 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
generics Issue is related to generics Proposal
Projects
Status: Declined
Development

No branches or pull requests