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: add generic programming using type parameters #43651

Open
ianlancetaylor opened this issue Jan 12, 2021 · 221 comments
Open

proposal: spec: add generic programming using type parameters #43651

ianlancetaylor opened this issue Jan 12, 2021 · 221 comments
Labels
Projects
Milestone

Comments

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 12, 2021

We propose adding support for type parameters to Go. This will change the Go language to support a form of generic programming.

A detailed design draft has already been published, with input from many members of the Go community. We are now taking the next step and proposing that this design draft become a part of the language.

A very high level overview of the proposed changes:

  • Functions can have an additional type parameter list that uses square brackets but otherwise looks like an ordinary parameter list: func F[T any](p T) { ... }.
  • These type parameters can be used by the regular parameters and in the function body.
  • Types can also have a type parameter list: type MySlice[T any] []T.
  • Each type parameter has a type constraint, just as each ordinary parameter has a type: func F[T Constraint](p T) { ... }.
  • Type constraints are interface types.
  • The new predeclared name any is a type constraint that permits any type.
  • Interface types used as type constraints can have a list of predeclared types; only type arguments that match one of those types satisfy the constraint.
  • Generic functions may only use operations permitted by their type constraints.
  • Using a generic function or type requires passing type arguments.
  • Type inference permits omitting the type arguments of a function call in common cases.

For more background on this proposal, see the recent blog post.

In the discussion on this issue, we invite substantive criticisms and comments, but please try to avoid repeating earlier comments, and please try to avoid simple plus-one and minus-one comments. Instead, add thumbs-up/thumbs-down emoji reactions to comments with which you agree or disagree, or to the proposal as a whole.

If you don't understand parts of the design please consider asking questions in a forum, rather than on this issue, to keep the discussion here more focused. See https://golang.org/wiki/Questions.

@ianlancetaylor ianlancetaylor added this to the Proposal milestone Jan 12, 2021
@ianlancetaylor ianlancetaylor added this to Incoming in Proposals Jan 12, 2021
@atdiar

This comment has been hidden.

@ALTree

This comment has been hidden.

@atdiar

This comment has been hidden.

@hanneshayashi

This comment has been hidden.

@tsal
Copy link

@tsal tsal commented Jan 12, 2021

Why any and not interface{} ?

an interface{} wouldn't make sense here since we're describing a generic trait that needs to be implemented. This is more meta-code and interface{} is still concrete - even if it is "generic" in some senses of the word.

I'm also having trouble thinking how you could implement any interface{} generic parameters, since you don't know what the interface will actually be - and if you're doing interface type-checking here, it's defeating the entire point of generics (IMO).

@coder543

This comment has been hidden.

@bcmills
Copy link
Member

@bcmills bcmills commented Jan 12, 2021

I remain concerned that this proposal overloads words (and keywords!) that formerly had very clear meanings — specifically the words type and interface and their corresponding keywords — such that they each now refer to two mostly-distinct concepts that really ought to instead have their own names. (I wrote up this concern in much more detail last summer, at https://github.com/bcmills/go2go/blob/master/typelist.md.)


Specifically, the word type today is defined as:

A type determines a set of values together with operations and methods specific to those values.

Under this proposal, I believe that a type would instead be either a set of values with operations, or a set of sets of values, each with its own set of operations.

And today the word interface, in the context of Go, refers to a type, such that:

A variable of interface type can store a value of any type with a method set that is any superset of the interface.

Under this proposal, a variable of interface type can store a value of any type with a method set that is any superset of the interface, unless that interface type refers to a set of sets of values, in which case no such variable can be declared.


I'd like to see more detail on the exact wording proposed for the spec, but for now I am against this specific design, on the grounds that the ad-hoc overloading of terms is both confusing, and avoidable with relatively small changes in syntax and specification.

@flibustenet
Copy link

@flibustenet flibustenet commented Jan 12, 2021

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

@nadiasvertex
Copy link
Contributor

@nadiasvertex nadiasvertex commented Jan 12, 2021

Would still prefer to have the empty interface. How tedious can this really be? Especially since we can define type aliases ourselves...

I think interface{} is a mistake. It is a hack to permit something like void * without any semantic cues to help a user understand what is going on. I would have preferred to have "any" as a type in the language from the beginning, even it it was just an alias for interface{} under the covers.

Of course, the new "any" is different than interface{}. It would be nice to have a named type that means "any type by reference" instead of "any type by substitution".

@michaelwilner
Copy link

@michaelwilner michaelwilner commented Jan 12, 2021

```go
func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

Valid point, in your example, an interface would be the better choice. However, a more apt use case of generics would be sort. Operations on slices of arbitrary types would be distinctly less verbose with generics as compared to interfaces. This talk touches on some of the points: blog.golang.org/why-generics (disclaimer: syntax in this link is different than this generics proposal, but the points are useful to draw comparison)

@JeremyLoy
Copy link

@JeremyLoy JeremyLoy commented Jan 12, 2021

What are the plans for amending the standard library to utilize generics? I see two important tracts of work here.

  1. retroactively applying generics to packages like sort, container/list
  2. Creating new packages and libraries that were previously cumbersome without generics; i.e. mathematical set functions,
@coder543
Copy link

@coder543 coder543 commented Jan 12, 2021

@bcmills

Specifically, the word type today is defined as:

A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The fact that a type is not just a struct is why the syntax in the language is type foo interface { ... }, type foo struct { ... }, and even type foo bar or type foo = bar.

A generic type is just as concretely a set of values, operations, and methods as an interface is (which is to say, you an argue that it isn't). So either we should redefine interface to not be a type (by the definition you're quoting), or we should accept that a generic type is also a type, just one that requires type parameters to be resolved before it becomes a concrete type.

If the proposal is misusing the term "type" in place of "type parameter" anywhere, I think that could be valid criticism... but it sounds like you're criticizing some ambiguous/arguably wrong terminology that exists in the Go language spec, which is terminology that is refuted by the language itself, as demonstrated by Go syntax above. If an interface is not a type, we should not prefix the declaration with the word type, but we do.

That whole area of discussion seems off topic here, and clarifications to the existing language spec could be proposed somewhere else? I've read through the generic proposal several times and I haven't come away feeling like the terminology used was ambiguous or confusing, and your statements here do not effectively make the case for that either, in my opinion.

@fzipp
Copy link
Contributor

@fzipp fzipp commented Jan 12, 2021

Of course, the new "any" is different than interface{}.

The new "any" is not different from interface{} as a type constraint. [T any] and [T interface{}] are interchangeable as per the proposal.

@IceWreck
Copy link

@IceWreck IceWreck commented Jan 12, 2021

What are the plans for amending the standard library to utilize generics? I see two important tracts of work here.

1. retroactively applying generics to packages like `sort`, `container/list`

2. Creating new packages and libraries that were previously cumbersome without generics; i.e. mathematical set functions,

Yes, and after generics are implemented, I hope the container package will be expanded to include other common data structures present in c++/java std libs

@zephyrtronium
Copy link
Contributor

@zephyrtronium zephyrtronium commented Jan 12, 2021

@bcmills

Specifically, the word type today is defined as:
A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

@fzipp
Copy link
Contributor

@fzipp fzipp commented Jan 12, 2021

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

I'd expect a linter warning: "useless use of type parameter"

@coder543
Copy link

@coder543 coder543 commented Jan 12, 2021

@bcmills

Specifically, the word type today is defined as:
A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

If you want to go down that route... the same exact thing applies to the "overloading" of "type" to refer to generic types as well. In order for a value to be substituted for a type parameter, it must implement the interface constraints, and to do that, it must be a concrete type. Therefore, a generic type "does indeed determine a set of values (always a superset of other types)".

It's the same thing. Either an interface is a type (in which case, it's fine for the proposal to use its current terminology), or it's not (in which case it's not okay for the Go language to define interfaces as types).

Either way, someone could propose that the Go language spec is written in a confusing way in the quoted section, but it wouldn't change any outcomes regarding this proposal or the current-day reality of Go.

@p-kraszewski

This comment has been hidden.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jan 12, 2021

@bcmills

Specifically, the word type today is defined as:
A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

In general, methods in Go are tied to type definitions and behave, in a lot circumstances, like any other function except that there's an argument placed before the function name. That's why, unlike most languages, Go allows you to call a method on a nil pointer no problem, meaning that you can handle the nil pointer case in the method itself instead of elsewhere.

Interfaces are a strange exception to this. Despite the fact that a type is defined, as in type Example interface { /* ... */ }, attempting to declare methods on that type will fail, purely because the underlying type is of kind interface. This dichotomy has always existed in Go, and it's always kind of bugged me, but it's a very minor thing that's basically never any kind of problem in practice, and I don't really think that the usage of interfaces in this proposal changes that much at all.

@ianlancetaylor

This comment has been hidden.

@knz

This comment has been hidden.

@griesemer
Copy link
Contributor

@griesemer griesemer commented Jan 12, 2021

@p-kraszewski The most up-to-date development is happening on the dev.typeparams branch. The dev.go2go branch was used to develop a prototype and the go2go playground; general development of that has been suspended in favor of a real implementation in dev.typeparams. But we hope to update dev.go2go occasionally to keep the go2go playground in reasonably good shape.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jan 12, 2021

Is it possible to create a chan T when T has constraint any? I did not find mention of channels in the section "Operations permitted for any type".

I think that you're confusing the declaration of a type parameter and the usage. The parameters are declared in function and type declarations and are essentially scoped to those functions and types. For contrived example,

//    declaration       usages
//        v             v    v
func Send[T any](c chan T, v T) {
  c <- v
}

Once they're declared, there basically isn't any difference in terms of usage between the type parameters and any other type, so they can be used as the element type of a channel, or the element type of a slice, or an argument to a function, or basically anything else.

@steeling

This comment has been hidden.

@knz

This comment has been hidden.

@stigsb
Copy link

@stigsb stigsb commented Jan 12, 2021

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

govet or golint could help with that.

@zikaeroh
Copy link
Contributor

@zikaeroh zikaeroh commented Jan 12, 2021

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

I'd expect a linter warning: "useless use of type parameter"

@fzipp That wouldn't be entirely accurate as a warning. Depending on the compiler's devirtualization pass, the two would have different performance characteristics. Assuming the "stenciling" approach for implementing generics, each version of foo would be expanded out to each specific type to generate the most efficient code, and the former would be faster as the compiler knows exactly what String to call, while in the latter the compiler may potentially box the value into an interface type and then have to look up the correct String for the type it gets.

@aarzilli
Copy link
Contributor

@aarzilli aarzilli commented Jan 18, 2021

I'm concerned about the examples under the "Constraint type inference" section. Let's take the FromStrings example.

In principle this should be a simple task, it's a function that wants to talk about a type T and the methods defined on its pointer type *T. Methods with a pointer receiver are basically universal in Go so this is something that will probably come up often.

The soulution, however, involves a number of new concepts: parametric constraints, type lists inside interfaces, using a type parameter in a type list, non-straightfoward type unification...

Also the section on complexity says:

We believe that the increased complexity is small for people reading well written generic code, rather than writing it

but I don't think this is true for this specific case (to be honest I'm not sure it can ever be true in the way it's written, but especially in this case). If I come across a function:

func F[A B, C D[A]](...)

and wish to call it, how many types do I have to specify? Is it two, A and C, with C being somehow related to A (for example C is a function type with an argument of type A), or is it just one type, A, and C only exists to talk about an additional constraint on a derived type of A?
To find out I have to read and understand the declaration of D, which will almost certainly involve understanding how type lists and parametric constraints interact.

I think reading generic code would be easier if one could write FromStrings like this:

func FromStrings[T any, *T Setter](s []string) []T { ... }

reading this it's clear that we are just talking about a type T and the second parameter is just there to specify additional constraints on a derived type.

Syntactically this means allowing a type literal, not just an identifier, to appear as the left-hand side of a type parameter. Semantically when the type parameter typeLit constraint appears in a type list, it would be equivalent to _ interface { constraint; type typeLit }.

A similar bit of syntactic sugar would help with the other example. If non-interface types could appear as constraints DoubleDefined could be written as:

func DoubleDefined[E constraints.Number, S []E](s S) S { ... }

Any time a non-interface constraint appears it would be equivalent to interface{ type constraint }.

@anjmao

This comment was marked as off-topic.

@Merovius

This comment was marked as off-topic.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Jan 18, 2021

@aarzilli We used to have an idea similar to what you suggest, but many people found it deeply confusing.

A problem with it is that *T is being listed as a type parameter, but it's not really a type parameter. It's an additional constraint on T. With constraint type inference we really do have additional type parameters, and the caller can pass type arguments for them. It's just that those type arguments can be inferred.

That said I agree that constraint type inference is one of the more complex aspects of the proposal, and it would be nice if we could simplify them somehow. But we don't want to confuse additional constraints with type parameters.

@aarzilli
Copy link
Contributor

@aarzilli aarzilli commented Jan 18, 2021

@ianlancetaylor

We used to have an idea similar to what you suggest, but many people found it deeply confusing.

Well, that's a shame. I hope you explore other syntactic options for this.

The current solution is very elegant, handling this cases as a side effect of the interaction between type lists and type unification. However, in my (limited) experience, people who are not already familiar with complex type systems find parametric constraints and type unification alien and difficult to understand.

You would not see this effect if the cohort who has tried generic go so far consists of people interested in PL design, because they are all already familiar with that.

What about the second part of my message, about allowing func DoubleDefined[E constraints.Number, S []E](s S) S { ... } ?

@martinrode
Copy link

@martinrode martinrode commented Jan 18, 2021

Let's take a moment to appreciate how amazing it is that this proposal can be contemplated for the 1.x release series of Go, and doesn't have to wait for Go 2.

From a marketing perspective this should better be called Go 2. What other planned change may ever better justify this step?

@Merovius
Copy link

@Merovius Merovius commented Jan 18, 2021

@martinrode From a marketing perspective, calling anything Go 2 also has downsides, in that it will lead people to make the same mistake @bobg did - assuming that it is a point breaking compatibility and that they have to choose a Go version to use/support, or anything. I don't really care if anything is called Go 2 or not, FWIW. But I feel like even talking about "Go 2" has strange effects on people's assumptions of how Go development and progression works.

@martinrode
Copy link

@martinrode martinrode commented Jan 18, 2021

@martinrode From a marketing perspective, calling anything Go 2 also has downsides, in that it will lead people to make the same mistake @bobg did - assuming that it is a point breaking compatibility and that they have to choose a Go version to use/support, or anything. I don't really care if anything is called Go 2 or not, FWIW. But I feel like even talking about "Go 2" has strange effects on people's assumptions of how Go development and progression works.

I understand your point, but I still disagree. Since this is going to be the biggest change to the language sine 1.0, I argue that it must be called 2.0. Many other big languages like Java or C++ regularly bump their major version numbers without breaking changes (I am not looking at you Python), so I say that is the default and its what people expect. So no worries let‘s do this now or never. Hiding such a great feature behind a one percent increase would be a shame.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Jan 18, 2021

@aarzilli

What about the second part of my message, about allowing func DoubleDefined[E constraints.Number, S []E](s S) S { ... } ?

Sorry for not mentioning that. I think what you are suggesting is a kind of syntactic sugar for a structural constraint. We permit an arbitrary type C to be used as a type constraint. When C is not an interface type, it is equivalent to the interface type interface { type C }. I know that @griesemer and I discussed this idea earlier, but I don't recall what our conclusions were. Maybe just that it is something we can consider doing later, as it is syntactic sugar.

Edited to add: to clarify, the paragraph above is not a description of the current proposal. It is my restatement of my understanding of what @aarzilli was suggesting in #43651 (comment).

@tooolbox
Copy link

@tooolbox tooolbox commented Jan 18, 2021

Many other big languages like Java or C++ regularly bump their major version numbers without breaking changes (I am not looking at you Python), so I say that is the default and its what people expect.

In the Go ecosystem, particularly Go Modules, v2 means breaking changes from v1. Therefore I think the best course is to preserve v1 for as long as there are no breaking changes. In ten years, if someone comes over from C++ and wonders why Go is on 1.37 he can be pointed to the Go 1 Compatibility Promise.

@joncalhoun
Copy link

@joncalhoun joncalhoun commented Jan 19, 2021

I was curious how much of a limitation not having methods would be so I started experimenting. I think you can simulate at least some of the behaviors without generic methods.

For instance, imagine you wanted some generic DB type so that you could mock methods like QueryRow with any return value.

type Scanner interface {
	Scan(dest ...interface{}) error
}

type DB[T Scanner] interface {
	QueryRow(query string, args ...interface{}) T
}

This can now be implemented with both *sql.Row and any custom Scanner implementation.

type MockScanner struct {
	ScanFn func(dest ...interface{}) error
}

func (ms *MockScanner) Scan(dest ...interface{}) error {
	return ms.ScanFn(dest...)
}

Now if you want to use this type inside something like a WidgetService you will probably have a generic methods issue.

type WidgetService[T Scanner] struct {
	DB DB[T]
}

// This won't work because we can't create generic methods.
func (ws *WidgetService) Create(w *Widget) error {
	err := ws.DB.QueryRow(`
INSERT INTO widgets (size, color) 
VALUES ($1, $2) 
RETURNING id`, w.Size, w.Color).Scan(&w.ID)
	if err != nil {
		return fmt.Errorf("create widget: %w", err)
	}
	return nil
}

One alternative is to write a Create function for your widgets that accepts our generic DB type.

func create[T Scanner](db DB[T], w *Widget) error {
	err := db.QueryRow(`
INSERT INTO widgets (size, color) 
VALUES ($1, $2) 
RETURNING id`, w.Size, w.Color).Scan(&w.ID)
	if err != nil {
		return fmt.Errorf("create widget: %w", err)
	}
	return nil
}

But, that means our WidgetService can't be used to implement interfaces. So if we had something like a server...

type Server struct {
  WidgetService interface {
    Create(w *Widget) error
  }
}

We can't use our generics type with that in any easy way.

One alternative is to use a struct with function fields.

// Still testable/mockable my swapping out each function field.
type WidgetService struct {
	Create func(*Widget) error
}

type Server struct {
	WidgetService WidgetService
}

Then you can simplify creation with some code like:

func NewWidgetService[T Scanner](db DB[T]) WidgetService {
	ws := WidgetService{}
	ws.Create = func(w *Widget) error {
    // create() here is the same create[T Scanner].. from above
		return create(db, w)
	}
	return ws
}

I have no idea what the implications of this is in terms of code readability, maintenance, etc, but at first glance I think I would still be able to write a lot of the code I currently write using an approach like this. It might not feel as elegant, but it should work.

Here is a playground link: https://go2goplay.golang.org/p/bluhM6TCG0R

What am I missing? I'm sure there is some edge case I'm not seeing.

PS - I realize the specific example I used with *sql.Row might be a weird one. I was going down that rabbit hole as another mental exercise when I realized these two issues overlap, and I just didn't change the example.

@Merovius
Copy link

@Merovius Merovius commented Jan 19, 2021

@joncalhoun I'm having trouble understanding your example. In particular, where you write This won't work because we can't create generic methods I don't understand why that wouldn't work. You can have methods on generic types - you just can't have those methods take new type-parameters. But in your case, that's not an issue - T is already declared as a type-parameter of WidgetService. Perhaps you can send a link to a playground-example of what you like to do with them, that doesn't work?

Generally, I'm skeptical that function-values as a struct field can address the use-cases prevented by the limitations on extra type-parameters for functions. That's because the restriction is in place for relatively fundamental implementation issues - and semantically, an interface value can also be viewed as a struct with function value fields. So if they could address the issues, we could just transfer that solution to methods and interface-satisfaction.

To be clear, I think to actually address the issue, you'd have to be able to transform something like this:

type X struct {
}

// Extra type parameter T, not mentioned in the declaration of X.
func (x X) F[T any](T) {
}

Into this:

type X struct {
    // Extra type parameter T, not mentioned in the declaration of X.
    F func[T any](T)
}

And this can't work - it would require you to get a function value of an uninstantiated generic function. This is prohibited by the design - for very similar reasons as why methods with extra type-parameters are prohibited.

@joncalhoun
Copy link

@joncalhoun joncalhoun commented Jan 19, 2021

@Merovius Is the Go2Go playground not up-to-date then? I couldn't find the correct way to add a method to a generic type. See https://go2goplay.golang.org/p/1vIb794CaBm for a broken example I made while experimenting.

@Merovius
Copy link

@Merovius Merovius commented Jan 19, 2021

@joncalhoun You have to mention the type-parameter in the receiver: https://go2goplay.golang.org/p/ZV873kDkeEf

@joncalhoun
Copy link

@joncalhoun joncalhoun commented Jan 19, 2021

Huh.. I swear I tried that unsuccessfully. 🤦‍♂️

Thanks! I misunderstood then - I was thinking methods on generic types weren't part of the proposal and was using closures to sorta hack around it.

I'd have to look at the case you are describing more closely, and about to jump in a zoom. Will look later though.

@bcmills
Copy link
Member

@bcmills bcmills commented Jan 19, 2021

@ianlancetaylor

When C is not an interface type, it is equivalent to the interface type interface { type C }.

I don't see that behavior described in the current draft of the design (perhaps I've missed it?), but if I understand that statement correctly I think that definition is not coherent with the rest of the design.

Both in the Featherweight Go paper and in the published Type Parameters draft design, a type constraint is an upper bound on the types that can be passed for the type argument: the actual argument is always a subtype of the constraint type, in the sense that an expression of the argument type can be used in any context that permits an expression of the constraint type. (As it is phrased in the draft design: “Calling a generic function with a type argument is similar to assigning to a variable of interface type: the type argument must implement the constraints of the type parameter.”)

However, the type C is not an upper bound on the types that match interface { type C }.

Consider the types C = int64 and D = time.Duration (https://go2goplay.golang.org/p/QN74Z87hDdb). Because the underlying type of time.Duration is int64, time.Duration matches interface { type int64 }. However, because a value of type time.Duration is not assignable to int64, time.Duration is not a subtype of int64, and int64 is not an upper bound for time.Duration.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Jan 19, 2021

@bcmills

I don't see that behavior described in the current draft of the design (perhaps I've missed it?), but if I understand that statement correctly I think that definition is not coherent with the rest of the design.

It's not in the current proposal. I was restating @aarzilli 's suggestion in my own words.

@carnott-snap
Copy link

@carnott-snap carnott-snap commented Jan 19, 2021

I could not find any discussion of structural typing, or "how do I ask for any struct (or struct pointer) with a field named X of type T?", in the proposal. Feel free to link me against it if that took place somewhere else.

I see that if you define a type constraint with structs that all have the same field name and type, that you can use it, but that requires enumerating all the possible input types, something that may not be feasible. I think some cases could be solved by getters, but this both adds complexity and may not be feasible for all cases.

@OneOfOne
Copy link
Contributor

@OneOfOne OneOfOne commented Jan 19, 2021

@bcmills note that you can do it anyway, https://go2goplay.golang.org/p/i_5dyMuTaCk

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Jan 19, 2021

@carnott-snap You are correct: the proposal does not provide a way to write a constraint for "any struct with field X of type T".

I think it might be possible to add such a constraint in the future. I don't know if that would ever actually happen, though. It seems like a special case. The contracts design draft did support it in a way, but that was more or less by accident, not because it seemed important.

@tooolbox
Copy link

@tooolbox tooolbox commented Jan 19, 2021

Having just re-read most of the design, the one thing I strongly object to is the use of structural constraints to allow omitting some of the type parameters at the instantiation site, as in the Pointer Method Example. I believe that instantiation should require all type parameters to be specified unless they can all be inferred. This will improve clarity, since the reader never has to wonder if he's looking at a partial set of type arguments. It also encourages API designers to aim for full inference since there's no halfway point.

With what I'm suggesting, the "Element Constraint example" would work, but not the "Pointer Method Example". If the latter turns out to be a common scenario and true pain point, the restriction could be relaxed in later versions.

@carnott-snap
Copy link

@carnott-snap carnott-snap commented Jan 19, 2021

As long as there is not fundamental opposition to the concept, that is fine. I do not have a solid use case for the need, but it sounds like if one came up, we could work to a resolution.

The biggest abstract concern I have is that lack of this feature will push people to use getters over fields. Currently the inverse is cannon, and we may not want to see that pattern change, or we may be fine with it, but we should be clear about the implications to style.

@firelizzard18
Copy link

@firelizzard18 firelizzard18 commented Jan 19, 2021

@OneOfOne Your example is not what @bcmills is talking about:

However, because a value of type time.Duration is not assignable to int64

In your example, you cast the value to int64 and then assign it to an int64 variable. @bcmills is saying that a value of type time.Duration cannot be assigned to a variable of type int64 without casting.

The use of generics does not change this behavior.

@ggaaooppeenngg
Copy link

@ggaaooppeenngg ggaaooppeenngg commented Jan 20, 2021

Since any equals interface{} and interface{} means an interface able to hold everything, it is common sense for the gophers that interface{} is a constrain without any method or item requirements when generics introduced. Go is famous for its small number of keywords and precluded alias. I think any may break its simplicity.

@atomsymbol
Copy link

@atomsymbol atomsymbol commented Jan 20, 2021

Since any equals interface{} and interface{} means an interface able to hold everything, it is common sense for the gophers that interface{} is a constrain without any method or item requirements when generics introduced. Go is famous for its small number of keywords and precluded alias. I think any may break its simplicity.

Nothing+optionalsemicolon would be more succinct to write than any or interface{}.

@ianlancetaylor
Copy link
Contributor Author

@ianlancetaylor ianlancetaylor commented Jan 20, 2021

@atomsymbol But permitting type parameters to not specify a constraint would be ambiguous, as discussed previously.

@deanveloper
Copy link

@deanveloper deanveloper commented Jan 20, 2021

Since any equals interface{} and interface{} means an interface able to hold everything, it is common sense for the gophers that interface{} is a constrain without any method or item requirements when generics introduced. Go is famous for its small number of keywords and precluded alias. I think any may break its simplicity.

we already have similar concepts like rune and byte. they are widely used even though they are aliases for other (even more shortly named than interface{}) types. i really don’t think that adding any is going to break any simplicity.

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Jan 20, 2021

@griesemer
You say:

any is predeclared as type any interface{} (it could be an alias, it doesn't matter because it's an interface).

But that's actually not true, because even interface types have identity.
This code doesn't compile, for example:

package main

type any interface{}

func main() {
	var g func() interface{}
	g = f
	g()
}

func f() any {
	return nil
}

It's important that any be declared as an alias for interface{}, in my view.

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Jan 20, 2021

Various people have suggested using a type-switch on an interface value to determine the actual type of a type parameter with code like this:

func F[T any]() int {
	var t T
	switch (interface{})(t).(type) {

I'll just note that this code won't work reliably for all possible types, because if T is an interface type, then t will have no type and the interface{} value will be nil.

For example: https://go2goplay.golang.org/p/V5aMUc4MoGc

Using a pointer to the type makes it reliable (although it's still not possible to switch on underlying types, of course): https://go2goplay.golang.org/p/b6V6xkFYmGB

Even when you've done such a type switch, you'll still need to use dynamic (and therefore error-prone) type conversion to access values of parameters and assign to return parameters. For example:

func splitBefore[T comparable](a []T, b T) (r []T) {
	switch interface{}((*T)(nil)).(type) {
	case *byte:
		// When T is byte, we can use the efficient bytes.IndexByte implementation.
		a := interface{}(a).([]byte)
		b := interface{}(b).(byte)
		i := bytes.IndexByte(a, b)
		if i == -1 {
			return nil
		}
		*(interface{}(&r).(*[]byte)) = a[i:]
		return
	}
	for i, x := range a {
		if x == b {
			return a[i:]
		}
	}
	return nil
}

Personally, one significant issue I have with the proposal is the way that the type matching on underlying types makes type switches on generic type parameters untenable. If there was no matching on underlying types, then a type switch on type parameters could be usefully used to implement efficient and type-safe ad hoc specialisation (for example to use efficient machine code implementations for specific known types).

It occurs to me that one possibility might be to allow type switches on type parameters, but only when those parameters aren't type-list interfaces (i.e. when there's no possibility of ambiguity between actual type and underlying type).

@atomsymbol
Copy link

@atomsymbol atomsymbol commented Jan 20, 2021

@atomsymbol But permitting type parameters to not specify a constraint would be ambiguous, as discussed previously.

"any]" -> "]"
"any," -> ";"
[a, b T, c U] -> [a, b T; c U]
[T1, T2 any] -> [T1, T2]

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

Successfully merging a pull request may close this issue.

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