https://github.com/golang/go/issues/43651#issuecomment-776944155 Skip to content Sign up * Why GitHub? Features - + Mobile - + Actions - + Codespaces - + Packages - + Security - + Code review - + Project management - + Integrations - + GitHub Sponsors - + Customer stories - + Security - * Team * Enterprise * Explore + Explore GitHub - Learn & contribute + Topics - + Collections - + Trending - + Learning Lab - + Open source guides - Connect with others + The ReadME Project - + Events - + Community forum - + GitHub Education - + GitHub Stars program - * Marketplace * Pricing Plans - + Compare plans - + Contact Sales - + Nonprofit - + Education - [ ] [search-key] * # In this repository All GitHub | Jump to | * No suggested jump to results * # In this repository All GitHub | Jump to | * # In this organization All GitHub | Jump to | * # In this repository All GitHub | Jump to | Sign in Sign up {{ message }} golang / go * Watch 3.5k * Star 81.8k * Fork 11.9k * Code * Issues 5k+ * Pull requests 276 * Actions * Projects 3 * Wiki * Security * Insights More * Code * Issues * Pull requests * Actions * Projects * Wiki * Security * Insights 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. Pick a username [ ] Email Address [ ] Password [ ] [ ] Sign up for GitHub 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 Jump to bottom spec: add generic programming using type parameters #43651 Open ianlancetaylor opened this issue Jan 12, 2021 * 397 comments Open spec: add generic programming using type parameters #43651 ianlancetaylor opened this issue Jan 12, 2021 * 397 comments Labels Proposal Proposal-Accepted Proposal-FinalCommentPeriod Projects Proposals Milestone Backlog Comments @ianlancetaylor Copy link Contributor @ianlancetaylor ianlancetaylor commented Jan 12, 2021 * edited 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. The text was updated successfully, but these errors were encountered: 1467 101 51 290 6 [?] 259 219 78 @ianlancetaylor ianlancetaylor added the Proposal label Jan 12, 2021 @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. Sign in to view @ALTree This comment has been hidden. Sign in to view @atdiar This comment has been hidden. Sign in to view @hanneshayashi This comment has been hidden. Sign in to view @tsal Copy link @tsal tsal commented Jan 12, 2021 * edited 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). 78 9 9 @coder543 This comment has been hidden. Sign in to view @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. 125 22 8 [?] 3 12 @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 ? 196 16 1 [?] 8 7 @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". 56 22 2 @michaelwilner Copy link @michaelwilner michaelwilner commented Jan 12, 2021 * edited ```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) 10 @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, 157 [?] 4 @coder543 Copy link @coder543 coder543 commented Jan 12, 2021 * edited @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. 42 11 2 @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. 42 @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 59 1 3 @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'). 2 1 @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" 142 19 16 10 [?] 4 @coder543 Copy link @coder543 coder543 commented Jan 12, 2021 * edited @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. 7 3 @p-kraszewski This comment has been hidden. Sign in to view @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. 6 [?] 1 @ianlancetaylor This comment has been hidden. Sign in to view @knz This comment has been hidden. Sign in to view @griesemer Copy link Contributor @griesemer griesemer commented Jan 12, 2021 * edited @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. [?] 33 4 @DeedleFake Copy link @DeedleFake DeedleFake commented Jan 12, 2021 * edited 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. 28 @steeling This comment has been hidden. Sign in to view @knz This comment has been hidden. Sign in to view @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. 24 [?] 1 @zikaeroh Copy link Contributor @zikaeroh zikaeroh commented Jan 12, 2021 * edited 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. 29 2 [?] 3 355 hidden items Load more... @DmitriyMV Copy link @DmitriyMV DmitriyMV commented Feb 10, 2021 * edited The discussion above and the examples imho show that non-interface constraints are not mature enough to be added to the language. Disagree. Without them it would be impossible to write, for example, generic sort function without resorting to creating wrapper objects around all primitive types. The example by @bcmills seems somewhat strange to me func [T Adder] Add(a, b T) T { return a + b } since not only T requires a and b to implement Adder but also requires them to be of the same type. So Add[Adder](42, "x") would lead to compilation error. @markusheukelom Copy link @markusheukelom markusheukelom commented Feb 10, 2021 @henryas I have argued in favour of this approached a couple of times. However it seems the Go authors (and others) view the following example as must have for generics: func Concat[T fmt.Stringer](list []T) string { // call .String() on each elem } Another example is the Graph example in the current proposal, where you can even do things we generics and methods what is not possible with interfaces alone. I (really) don't understand the need for calling methods on T. I understand the example and I agree that you can't do it directly with interface, but I don't think I would need it ever. There's other solutions possible. I think the scope of generics should be limited to generic container (including maps) and functions that can use operators on builtin type. So if you drop the ability to call methods on T (and therewith all the brain melting stuff of wanting T to be a pointer etc), constrains can just be type lists, for example: // sketch type Numbers (float64, float32) // numbers is a typelist; a typelist can only include built-in boolean, numeric and string types type Any (=) // as a special case, (=) denotes all types type (==) // as a special case, (==) denotes all comparable types; It would be nice and simple, wouldn't overload the interface conceptually and syntactically while giving probably 95% of what generics wil be used for. At the same time the problem of choosing whether to use fmt.Stringer as interface value or type parameter constraint wouldn't even exists. 2 @andig Copy link Contributor @andig andig commented Feb 10, 2021 3. we don't do any of this proposal because this turns out to be a fatal conflict. That would be a great loss. It seems the proposal above is largely accepted with the exception of: 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. ...which seems what almost the entire discussion above was focused on. 1 @DmitriyMV Copy link @DmitriyMV DmitriyMV commented Feb 10, 2021 @Merovius So, AFAICT, either 1. we find a new solution to make type-lists acceptable, or 2. we do what you (and I as well) suggested and leave type-lists out of the first version or 3. we don't do any of this proposal because this turns out to be a fatal conflict. Not really. ISTM that whole discussion around "type lists" is based on the thought that #41716 is accepted or any type of sum-types will be based on current constraint syntax. I remain unconvinced that this would be the case. 1 @Merovius Copy link @Merovius Merovius commented Feb 10, 2021 @markusheukelom I think the scope of generics should be limited to generic container (including maps) and functions that can use operators on builtin type. This seems very restrictive. Note that, again, containers need to be able to constraint their type-arguments as well. You mention a map as a container, but even that requires at minimum to be able to compare keys. So, with this restriction, you could not use composite types as map-keys. Or elements in sorted sets or heaps. Meaning you can't, for example, have a scheduler that keeps a heap of time.Time for the next task it wants to kick off. Then there's also use-cases where you use a non-composite type, but you don't want its sorting order to be determined by the one defined by Go. For example, you might want different sorting-order on strings, based on localization settings. Most Go code uses at least some composite types. It seems critical to support writing generic code that can use them. @andig That would be a great loss. Which is why we all focus on 1 :) Again, I'm not saying any of the option is "best" or will, or should happen. I'm just saying that the options and implications seem clear, so there isn't really a need to discuss this further. The answer to "could we drop type-lists" is "yeah, probably" and that's all that needs saying. 1 @Merovius Copy link @Merovius Merovius commented Feb 10, 2021 @DmitriyMV That doesn't seem to contradict what I was saying. All that is implying is that we might have already achieved good enough type-lists (option 1). In that case, there isn't a need to discuss that any further. But it seems enough people disagree to keep the conversation going (though, TBF, a lot of the discussion is just re-hashing things already discussed here or in #41716). And talking about the problem space does not mean the proposal can't be accepted in the meantime - as-is or without type-lists or with a new version of type-lists that will be published later. And wanting to see it accepted doesn't mean we can't also criticize aspects of it and trying to improve them. The best we can all do, to bring this forward, is to try and bring up new ideas or arguments that could sway aspects of the proposal. If the only thing standing in the way are type-lists and if you feel type-lists are fine as they are (personally, I disagree. But that's just an opinion), then it's fine to lean back. [?] 1 @bcmills Copy link Member @bcmills bcmills commented Feb 10, 2021 @Merovius if a) we leave the proposal as is and b) add "type-list interfaces as sum-types" later, but imply that such an interface can not be used to instantiate a generic function/type where it's used as a constraint, everything seems to [work] out fine I do not agree that "such an interface can not be used to instantiate a generic function/type" is a result that "seems to work out fine". (If type-list interfaces are interface types, then they should be usable wherever an interface type in general can be used, and have the same semantics.) if we would require the constraint to be explicit - wouldn't that just mean that every function using type-list constraints before "sum-types" want to add it pre-emptively, to avoid breaking once sum-types are introduced? s/want/need/ -- those functions would not even compile without it. (Otherwise, adding sum types would be a breaking change.) But, yes: every function that uses type-lists for the purpose of enabling operators in generic code would need to use the "is a concrete type that implements" form of constraint, not the plain "implements" form. @bcmills Copy link Member @bcmills bcmills commented Feb 10, 2021 @DmitriyMV not only T requires a and b to implement Adder but also requires them to be of the same type. So Add[Adder](42, "x") would lead to compilation error. The point of that example is to illustrate what would happen if, per http://golang.org/design/go2draft-type-parameters# type-lists-in-interface-types: Interface types with type lists may only be used as constraints on type parameters. ... This restriction may be lifted in future language versions. The example illustrates that the restriction cannot be lifted in any future language version. The type Adder -- which is allowed and well-defined under the proposal -- cannot be used as an ordinary interface type, because the meaning of the constraint T Adder for a type-list interface Adder would be different from the meaning of that constraint for an ordinary interface type. @ianlancetaylor Copy link Contributor Author @ianlancetaylor ianlancetaylor commented Feb 10, 2021 @atdiar Sorry, I'm not sure I understand your comment about the contracts design draft. We're not pursuing that approach. @Merovius Copy link @Merovius Merovius commented Feb 10, 2021 * edited @bcmills If every function that uses a type-list constraint will say "a concrete type implementing this interface" and every function that uses a pure interface constraint will say "any type implementing this interface (including itself)", I do not understand why we need a syntactical way to signify the difference. It seems to me, that the presence of a type-list in the constraint is signifier enough. I don't believe there is a need to be able to express "must be a concrete type" - either with, or without sum-types. Before sum-types, an interface can be used to instantiate a generic function if and only if it does not contain a type-list. We can keep this rule when introducing sum-types. As far as I can tell, everything would then work exactly the same, as if we introduce such a signifier and every function that uses type-lists adds the signifier and every function that doesn't leaves it out. The advantage is, that functions that use type-lists before we introduce sum-types don't need to use the signifier (i.e. we don't have to make this decision before we decide to introduce type-lists). I do not agree that "such an interface can not be used to instantiate a generic function/type" is a result that "seems to work out fine". But that's the same result if that function adds the "not an interface" signifier. And I still think the set of functions that would add those is the same set as the set of functions that uses type-lists. If type-list interfaces are interface types, then they should be usable wherever an interface type in general can be used, and have the same semantics. Maybe this is where we're just as an impasse then. To me, the answer is "sum-types shouldn't be interfaces then". Either by a) removing type-lists from interfaces, putting them into a new declaration which can then be shared, or b) not making type-list interfaces types (and, optionally, introduce a different sum-type concept). To me, it seems the utility of a "must not be an interface type itself" signifier seems to be rooted in being able to make an argument that sum-types and interfaces and type-list constraints are the same thing. But if we need to put so much work into making that argument, then maybe the natural conclusion is just that that's not the case. 1 @DmitriyMV Copy link @DmitriyMV DmitriyMV commented Feb 10, 2021 @Merovius All that is implying is that we might have already achieved good enough type-lists Not really. But I proposed solutions seems to be either: 1. implement constraints in terms of defining allowed operations set in addition to allowed methods set and not in terms of type lists. 2. remove type-lists and effectively disallow generics for primitive types 3. adjust type-lists to allow constraints in terms of "is type" in addition to "implements type". I do think that given Go history option 1 is off the table. The option 2 could work, but will hinder generics usage for primitive types, which are important for generic collections and generic functions. The option 3 is actually looking fine, but can be added further down the road. I do think that more data is required tho, thats why I'm in favor of accepting proposal as-is, and adjusting it down the road if necessary. @bcmills The more I think about, the more I agree that The type Adder -- which is allowed and well-defined under the proposal -- cannot be used as an ordinary interface type The thing is - while "theoretically infinite" set of types make sense for generics constraints, it doesn't work for sum-types or interface types, because any matching would require exhaustive list of allowed types. Although we could allow different meanings for type-lists depending on usage (in constraint, or in declaration), but it would just result in more confusion. @henryas Copy link @henryas henryas commented Feb 10, 2021 You can still create maps without constrained generics. func (m *CustomMap) Set[V](key Hasher, v V){} func (m CustomMap) Get[V](key Hasher) V {} //where type Hasher interface { Hash() int } We use interface if we need a specific behavior. func Concat(items ...Stringer) string { //call String on each item } It should solve many headaches mentioned above. The only issue is the map key isn't restricted to any specific concrete type, but I don't think that is a problem. The extra flexibility may be useful. I find the Adder illustration is a bit unrealistic. If you can do 1+1, why do you need Add(1,1)? Also when you are working with numbers, you need to be specific with the types and what to do with the overflow, decimals, negative numbers, and stuffs. I don't think it is a good idea to generic-ized (if there is ever such a word) such operations. You may have func Compute(int,int) int and let people with floats handle their own rounding. However, it may be useful to have primitive types implement basic interface to allow basic comparison such as: type NumericalComparer interface { CompareNumber(interface{}} int } type StringComparer interface { CompareString(string) int } 1 1 @bcmills Copy link Member @bcmills bcmills commented Feb 10, 2021 * edited To me, it seems the utility of a "must not be an interface type itself" signifier seems to be rooted in being able to make an argument that sum-types and interfaces and type-list constraints are the same thing. But if we need to put so much work into making that argument, then maybe the natural conclusion is just that that's not the case. I would be fine with that conclusion. But if we conclude that type-list constraints are not the same thing as interface types, then I think it's still too confusing to use the keyword interface to denote those two semantically-different things. If we go that route, I would prefer that we find a syntax (and specification) for type-lists that avoids the interface keyword and avoids describing type-lists as "interfaces". 4 @DmitriyMV Copy link @DmitriyMV DmitriyMV commented Feb 10, 2021 @Merovius In addition: type-lists for interfaces that are not used as constraints, do not make a lot of sense, since there is no way to specify the list of allowed operations. That is - you can't define a function the accepts two arguments of the typeAdders and sums them using + operator. 1 @atdiar Copy link @atdiar atdiar commented Feb 10, 2021 * edited @atdiar Sorry, I'm not sure I understand your comment about the contracts design draft. We're not pursuing that approach. @ianlancetaylor I meant to say that such unsatisfiable contracts are valuable because they represent a union of type constraints that a type can intersect its own constraints against. If the result is itself, it means that the type was one of the list. In the case we would want typelists as discriminated unions, and even in more general ways, such discriminated unions of incompatible set of constraints have value. The same way, if we want to say that a type T does not implement an Interface I, we would define the constraints as those of T and the complement of I. Complement of I is not satisfiable because it is the universe of all constraints except for the method set of I (and its name potentially) . Basically we can establish a new constraint: T intersect (Not I) = T written informally. @markusheukelom Copy link @markusheukelom markusheukelom commented Feb 10, 2021 @Merovius I think the scope of generics should be limited to generic container (including maps) and functions that can use operators on builtin type. This seems very restrictive. Note that, again, containers need to be able to constraint their type-arguments as well. You mention a map as a container, but even that requires at minimum to be able to compare keys. Yes of well course I agree on maps, the sentence is not so clear I admit. That's why I sketched that a special constrain (==) allows to restrict T to comparable types. The other special (=) would allow allow types (assignment being available for all types). See my OP. Please note that my main point is against calling methods on T. You can use any type for T, any comparable, or restrict it to any build-in boolean, string or numeric type (or type that has that as underlying type). But you cannot call methods on T. So, with this restriction, you could not use composite types as map-keys. Or elements in sorted sets or heaps. Meaning ?> you can't, for example, have a scheduler that keeps a heap of time.Time for the next task it wants to kick off. As said, actually there would be no such restriction in my sketch. Just use the special constraint (==) or (=). If you use T (=) you cannot use map[T]... in your type of course. Then there's also use-cases where you use a non-composite type, but you don't want its sorting order to be determined > by the one defined by Go. For example, you might want different sorting-order on strings, based on localization settings. Well just use a function. What's wrong with sort.Strings[S (string)] (list S[], func(a, b S) bool) in your example? A priority queue can all just ask for a func Priority(T) int or something. Does it pay-off against all of the downsides I mentioned by overloading the interface syntax/construct? I would even say that a generic container that asks for an extra function is clearer than using an interface constraint. Besides opinions, my technical argument here is that you CAN create a Heap[T] using the func helper. Without generics you can't do Heap[T]. You also cannot make a Min[T](a, b T) for any numeric type. So generics should allow you do the latter two (very common) use cases, but the first is not strictly needed. You can write a program with the same type safety without having call a method on a generic T. Therefore, I believe it is better to go without. It's simpler and you already have to great tools: passing an extra free function, or use an interface type. Most Go code uses at least some composite types. It seems critical to support writing generic code that can use them. I never said that I think... 2 @atdiar Copy link @atdiar atdiar commented Feb 10, 2021 * edited The problem with typelists as they are now is that there is structural constraints mixed with nominal constraints. Structural constraints do not enforce type names so the typelists, if it were used as an interface, would not be a closed set of types. An interface in a typelist could be salvaged. Interfaces can be named types. A builtin type, currently is problematic. Now if people think that they will never need a sound design for heterogeneous safe collections, sumtypes etc, or that there is a need for an explicit sumtype keyword, ok. I'm a bit afraid that this would be wrong. Even if it's never implemented eventually, I think it should be taken into account. They are too linked of an issue. types are the implementation of constraint sets. The proposal is about using them as the constraint sets themselves. It seems to be problematic. To constrain a type parameter, we need lists of type constraints, not list of types. We still need lists however. @Merovius Copy link @Merovius Merovius commented Feb 10, 2021 * edited @henryas The only issue is the map key isn't restricted to any specific concrete type, but I don't think that is a problem. It is, however, the problem we are trying to solve with generics. We want (among other things) type-safe containers. I agree that if we don't want type-safety, we don't need generics, much less constrained ones. The extra flexibility may be useful. There is no "extra" flexibility. Your example is equivalent to a *CustomMap[Hasher, V] in this proposal. So the type-parameter proposal awards you the same flexibility, if you want it. @markusheukelom Well just use a function. What's wrong with sort.Strings[S (string)](list S[], func(a, b S) bool) in your example? You can in fact transform a function with an interface constraint into this form, by using method expressions for all the methods and passing them as the extra arguments. For example, you can have type Lesser[T any] interface { Less(T) bool } func SortConstraint[T Lesser[T]]([]T) { // ... } func SortFuncs[T any](s []T, less func(T, T) bool) { // ... } type MyType int func (a MyType) Less(b MyType) bool { return a < b } func main() { var s []MyType SortConstraint(s) SortFuncs(s, MyType.Less) // Method expression } This transformation is always possible, so yes, that definitely works. What's "wrong" with it, is that this seems to pay a hefty price in convenience - having to list the operations on each call - for very little benefit - avoiding defining interface-constraints, which seem straight forward and easily understandable. Personally, I feel the tradeoff in this question pretty significantly favors interface-constraints. YMMV, we can agree to disagree on this. Does it pay-off against all of the downsides I mentioned by overloading the interface syntax/construct? I don't think there is any overloading happening - leaving aside type-lists (which, as I said a couple times, I'm not a fan of myself. Even though I see the value they bring to the table). 1 @markusheukelom Copy link @markusheukelom markusheukelom commented Feb 10, 2021 @Merovius We can certainly agree to disagree, any it seems others agree with you. I just don't understand it (yet, maybe), I think I could be missing a big point you and other are seeing. But I don't really see it, although I would be really happy to be proven wrong. For example I would never write this: type Lesser[T any] interface { Less(T) bool } func SortConstraint[T Lesser[T]](list []T) { // ... } Because, in practice that means you cannot sort list in a different order than that provided by T.Less(). So, func Sort[T any](list T[], less func(a, b T) bool) Is far better. The same argument applies to: type Heap1[T Lesser] struct { items []T } // vs type Heap2[T any] struct { Less func(T, T) bool items []T } It seems you want to favor Heap1. But what if I want two Heap1s on T, each with a different sorting? That would not be possible? So Heap2 is far better I think. Am I missing your point? Can you elaborate or give me an example? @Nathan-Fenner Copy link @Nathan-Fenner Nathan-Fenner commented Feb 10, 2021 @markusheukelom The part of your analysis that's missing is based on best practice today. In particular, what "best practice" may change (or need to be extended) in order to deal with new language features like generics. In particular, it's true that just passing a func(T, T) bool comparison to e.g. Sort is more flexible if you're comparing an arbitrary type, but that's not the only way that such code will be used. --------------------------------------------------------------------- Once it's possible to write generic collections, compositionally defining e.g. Less as a receiver function makes for less code to audit when you're looking for a bug. For example, if we have type List[T Lesser] []T func (list List[T]) Less(other List[T]) bool { for i := range list { if i >= len(other) { break } if list[i].Less(other[i]) { return true } if other[i].Less(list[i]) { return false } } return len(list) < len(other) } type Pair[T Lesser] struct { Left T; Right T } func (p Pair[T]) Less(q Pair[T]) bool { if p.Left.Less(q.Left) { return true } if q.Left.Less(p.Left) { return false } return p.Right.Less(q.Right) } we can now build e.g. a type List[List[Pair[List[Identifier]]]] and the correct Less implementation is provided automatically. Attempting to write it yourself from scratch would be rather difficult - you'd almost certainly make a mistake. Writing it with a collection of combinators would certainly be possible (and in some cases, more flexible!) but in some cases the extra rigidity and consistency of always getting the same behavior out of your composite collections is more convenient and more likely to lead to correct code. For example, if some entities e.g. Names should always be ordered ascending, you can implement that in their Less, and not need to ever worry about it again - this one unit of domain-specific business-logic can be written once, and reused repeatedly without the possibility for error. On the other hand, you might want e.g. TimeStamps to be always ordered descending, and you can also implement that in their Less function. If they're just treated as string, and it's the caller's responsibility to decide which to use at any given point, they have to make more decisions, which means they could end up making the wrong one. Of course they'd still be able to use the func(T, T) bool version, and there's no reason we can't have both. But the compositional construction of collections and the ability to enforce (or at least guide towards) correct domain-specific usage of certain types is an advantage of using receiver functions attached to types, which by their nature are always canonical. @Merovius Copy link @Merovius Merovius commented Feb 10, 2021 @markusheukelom I agree that that's an advantage. Note that the type-parameter design with constraints doesn't prevent us from writing that code. We can provide both - giving us the convenience of not having to type the extra arguments if we are fine with the order the method imposes and the flexibility to use custom orders if we need it. You can get the benefits of explicitly passing functions in both cases, but you only get the convenience with constraints. 2 @rsc Copy link Contributor @rsc rsc commented Feb 10, 2021 Lots of good discussion here, and the focus on specific details serves to highlight that we need to accept this proposal and then move on to detail-specific proposals. Thanks everyone. 9 3 @rsc Copy link Contributor @rsc rsc commented Feb 10, 2021 No change in consensus, so accepted. This issue now tracks the work of implementing the proposal. -- rsc for the proposal review group 201 135 [?] 54 75 9 @rsc rsc moved this from Likely Accept to Accepted in Proposals Feb 10, 2021 @rsc rsc changed the title [DEL:proposal: spec: add generic programming using type parameters:DEL] [INS:spec: add generic programming using type parameters:INS] Feb 10, 2021 @rsc rsc added the Proposal-Accepted label Feb 10, 2021 @rsc rsc modified the milestones: Proposal, Backlog Feb 10, 2021 @markusheukelom Copy link @markusheukelom markusheukelom commented Feb 10, 2021 @Nathan-Fenner That's a great example, thanks. It's clearly something that is daunting to write without calling methods on T. The drawback is of course you can't use List[T] on even float64 or int or string, even though these types have built-in <. Yes you can define a type MyInt int, but having to do that just to use a generic container.... This 'cost' is all spend for the use case of easily supporting List[List[Pair[List[Identifier]]]], while not supporting 'int', float64. I wonder if that's the correct tradeoff. So let me ask it this way: give me an example of where calling a method on T will be used in the standard Go library (updated to use generics). If there is no such obvious application, I doubt calling methods on T should be part of any generics proposal. @christopher-dG christopher-dG mentioned this issue Feb 10, 2021 More idiomatic generated code christopher-dG/go-obs-websocket#1 Open Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment Assignees No one assigned Labels Proposal Proposal-Accepted Proposal-FinalCommentPeriod Projects Proposals Accepted Milestone Backlog Linked pull requests Successfully merging a pull request may close this issue. None yet 96 participants @benburkert @davecheney @mdempsky @azazeal @willfaught @rogpeppe @beoran @ConradIrwin @rsc @serberoth @seebs @stigsb @magical @andig @aarzilli @DeedleFake @thomasf @bobg @jimmyfrasche @igilham and others * (c) 2021 GitHub, Inc. * Terms * Privacy * Security * Status * Docs * Contact GitHub * Pricing * API * Training * Blog * About You can't perform that action at this time. You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.