-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: allow type parameters in methods #49085
Comments
The document also explains what the problems are. So what are your solutions to these? |
This proposal is good to define an |
This proposal is a non-starter unless someone can explain how to implement it. |
@ianlancetaylor from the generics proposal
I think this solution makes the most sense. They could then (under the hood) be treated a regular function. The reason why this would be useful is that methods do not only serve the purpose of implementing interfaces; methods also serve as a means of organization for functions that operate on particular structures. It may be a bit of a challenge about how type-parameterized methods would appear in |
The problem would be simpler if the parameter type possibility set is known at compile time,
One new problem I'm aware of is there might be many methods have the same name for a certain type. |
If we're ultimately talking about multiple dispatch, the languages that really cater towards this do a massive amount of overloading. One language I find fun and interesting with this style is Julia, where things like Particularly for stream-to-stream conversion, I do think that the List Transform example is useful. We have to provide a concrete I think it's also often possible to have a higher-order function that generates conversion functions, while more specialized conversion functions are also naturally expressible in Go. Example: a very generic color conversion API might specify an interface with |
Even just that would allow for, for example, an iterator implementation, though it does require wrapping it in another type because of the lack of extension functions: type Nexter[T any] interface {
Next() (T, bool)
}
type NextFunc[T any] func() (T, bool)
func (n NextFunc[T]) Next() (T, bool) {
return n()
}
type Iter[T any, N Nexter[T]] struct {
next N
}
func New[T any, N Nexter[T]](next) Iter[T, N] {
return Iter[T, N]{next: next}
}
func (iter Iter[T, N]) Map[R any](f func(T) R) Iter[R, NextFunc[R]] {
return New(NextFunc[R](func() (r R, ok bool) {
v, ok := iter.next.Next()
if !ok {
return r, false
}
return f(v), true
})
}
// And so on. Usage is still awkward without a short-form function literal syntax, though, unfortunately: s := someIter.Filter(func(v int) bool { return v > 0 }).Map(func(v int) string { return strconv.FormatInt(v, 10) }).Slice()
// vs.
s := someIter.Filter(func(v) -> v > 0).Map(func(v) -> strconv.FormatInt(v, 10)).Slice() |
This will add so much complexity |
@batara666 can you explain why? adding type parameters to methods doesn’t seem like it’d add that much complexity to me. |
I think before we can think about if and how to do this, we should first address the "no higher level abstraction" restriction of generics, i.e. the inability to pass around a generic type/function without instantiation. The reason is that if we allowed additional type parameters on methods, we'd also de-facto allow to pass around generic functions without instantiation: type F struct{}
func (F) Call[T any] (v T) { /* … */ }
func main() {
var f F // f is now de-facto an uninstantiated func[T any](T)
} Therefore, to allow additional type-parameters on methods, we also have to answer how to pass around uninstantiated generic functions. Moreover, if we'd allow passing around uninstantiated generic types/functions, we could already build the abstractions given as motivations in the proposal-text. So, given that solving "no higher level abstraction" is a strictly easier problem to solve, while providing most of the benefit of solving "no additional type parameters on methods", it seems reasonable to block the latter on solving the former. Lastly, I'd urge everyone to consider that these limitations where not left in the generics design by accident. If they where really that easy to solve, the solution would have been in the design to begin with. It will take some time to solve them. |
The "type" concept almost means a memory layout. A value of a subset type could be passed/assigned to a superset type. For Go, the change would be too large. |
I was (far too) pleased (with myself) when I figured out this is possible: https://gotipplay.golang.org/p/1ixYAwxwVss The part about lifting values into type system 'symbols' feels like a bit DIY and tricky, not sure there isn't something better here. I did feel like the dispatch() call is technically interesting. Inferring from the function/method supplied to dispatch() wasn't obvious to me at first. Without overloading methods, just different instantiations of the dispatch() function, it is plausible to arrive at the correct dispatch over a set of concrete implementations. |
I'm putting this proposal on hold until we have more familiarity with the current generics implementation. |
Playing with a new library that intensively uses generics, I provided an equivalence implementation between a Method and a Function: At the end, the difference is only where the parameter is placed (as a receiver, or as a first argument), but the function allows you map to a Stream of different type and with the method you can only generate streams of the same type. With the code from the above link, I verified that this compiles: type Mapper[IT, OT any] func(Stream[IT], func(IT)OT) Stream[OT]
var _ Mapper[int, float64] = Map[int, float64] Simplifying, and obviating some internals from Go that I might ignore, I could see the generic In order to overcome the No parametrized methods issue pointed by @fzipp, from my partial view, I think that the example issue can be approached the same way as Java does: using func CheckIdentity(v interface{}) {
if vi, ok := v.(p2.HasIdentity); ok {
if got := vi.Identity[int](0); got != 0 {
panic(got)
}
} Would be translated to something equivalent to: func CheckIdentity(v interface{}) {
if vi, ok := v.(p2.HasIdentity); ok {
if got := vi.Identity(0).(int); got != 0 {
panic(got)
}
} Then the third line would panic if the In this case, we are translating the error check from the compile time to the runtime, but anyway this is what we actually have now if lack of parametrized methods forces us to continue using unsafe type castings. |
In your rewrite of |
This is a bad idea in my opinion - Type erasure is one of the most annoying limitations of generics in Java. I think it would go against the grain of the simplicity that Go aims for. |
I don't really have a horse in this race, but I find this proposal interesting and wanted to put down my thoughts. Based on what I see here: https://go.godbolt.org/z/1fz9s5W8x func (x *SomeType) Blah() { /* ... */ } And this: func Blah(x *SomeType) { /* ... */ } compile to nearly identical code. If we have a type type S struct {
/* ... */
} ...and func (s S) DoThing[T any](arg T) { /* ... */ } ...then we effectively have a generic function with the signature: func DoThing[T any](s S, arg T) { /* ... */ } Of course, if we have a generic type type G[T any] struct {
/* ... */
} ...and func (g G[T]) DoStuff[U any](arg U) { /* ... */ } ...then we effectively have a generic function with the signature: func DoStuff[T, U any](g G[T], arg U) { /* ... */ } In order to use either of these "functions", all of the type parameters have to be known. That means that in the case of Within this limited context, it seems to me like it wouldn't be a huge leap to allow type parameters on methods - it ends up referring to what is essentially a generic function, and we know things about generic functions:
The mechanism by which this interacts with interface definitions/implementations is less clear to me. Though, I think it is reasonable to say that a generic interface can't be implemented directly - it must be instantiated first. I'm not as sure of this, but it seems that it might also be true that an interface can only be implemented by a fully instantiated type. Even in code like this: type GenericInterface[T any] interface {
Foo() T
}
type GenericStruct[T any] struct {
Bar T
}
func (g GenericStruct[T]) Foo() T {
return g.Bar
}
func MakeGeneric[U any]() GenericInterface[U] {
return GenericStruct[U]{}
} It seems like, within the context of One area that seems complex is interfaces whose methods have type parameters. For example, if we had: type Mappable[T any] interface {
Map[U any](func(T) U) []U
} What would it mean to "instantiate" It seems much simpler to disallow that kind of interface entirely, and just require something like: type Mappable[T, U any] interface {
Map(func(T) U) []U
} I think that could still be just as useful, depending on how interface implementation is handled when the underlying type has methods with generic parameters. As an example: // Slice[T] provides slice operations over a slice of T values
type Slice[T any] []T
// Map[U] maps a Slice[T] to a Slice[U]
func (s Slice[T]) Map[U any](func (T) U) Slice[U]
type Mappable[T, U any] {
Map(func (T) U) Slice[U]
}
// In order for this assignment to be valid:
// 1. Slice[int] must have a method named Map ✅
// 2. Slice[int].Map must have the same number of arguments ✅
// 3. Slice[int].Map must have the same number of returns ✅
// 4. Slice[int].Map must have the same types for each argument and return ???
var _ Mappable[int, float64] = Slice[int]{1,2,3} It seems reasonable to me to say that In this case, assuming that methods with type parameters are allowed, I would think the compiler could do something like:
If you're calling the method an the interface object, then you only have access to that one particular instantiation of To summarize: Given that it is a feature of go that interface implementation can be tested at runtime via type assertions, reflection, etc, I don't see any way around banning generic methods on interface definitions. However, because methods are more or less sugar for functions, it seems to me it would be possible to allow generic parameters on methods of concrete types, and to allow these methods to participate in implementation of fully instantiated interface types. |
I don't think you are solving the problems from the design doc, though:
This fulfills all your criteria, it uses no parameterized interfaces and It is very easy to look at the proposal text and think "this would be a useful feature to have, I obviously would like it in the language". But because it's such an obvious feature to put in, it would be great if people ask themselves why the Go team didn't put it in in the first place. Because there are reasons and these reasons need answering. |
I would like to drop an idea here which I think can be useful for the "type parameters in methods" topic. Maybe it has an obvious flaw I haven't seen or it has already been considered or discussed, but I couldn't find anything about it. Please, let me know if so. With the current proposal, we can't have type parameters in methods but, couldn't we achieve the same effect if we put the type parameters in the type definition? I mean, instead of doing this: type Slice[T any] []T
func (s Slice[T]) Map[U any](func (T) U) Slice[U] Do this (move the U type parameter from the method "Map" to the struct): type Slice[T any, U any] []T
func (s Slice[T,U]) Map(func (T) U) Slice[U] @Merovius Wouldn't this solve the issue you mentioned in the above comment? Your example would end up like this: type IntFooer interface { Foo() int }
type StringFooer interface { Foo() string }
type X[T any] struct{}
func (X) Foo() T { return *new(T) }
func main() {
var x X[int] // You are forced to specify the type parameter here with the current proposal. I guess it could be inferred it this were an initialization instead of a declaration only
x.(StringFooer) // This would fail, as it doesn't conform to the interface
x.(IntFooer) // This would pass
reflect.ValueOf(x).MethodByName("Foo").Type() // "Foo(X) int"
} I think this would work with the current proposal without changes. As I said, I could be missing something obvious here. Let me know if that's the case. |
You can easily do this, but it's not the same effect. People who want this specifically want the type-parameter of the method to be independent of the type itself, to implement higher-level abstractions. |
I see, thanks. After thinking it twice, I now see that what I propose would be very limiting as, after instantiating the type, you could not call the method with different type parameters (for example, call "map" with a function that return strings one time and then another time with a function that return ints). All right, I know there was something obvious here. Thanks for the response! |
I'm sure that's true, I probably would benefit from reviewing it again. To be fair, though, I wasn't trying to put together a concrete proposal - I understand why this is a complex topic and why it isn't in the first pass at generics, and why it may never be added to the language. I don't have a horse in this race beyond the fact that I find this interesting. My intent was to think out loud about what restrictions might make this more concretely approachable. Also, for what it's worth, I think that disallowing parameterized methods on interface types could be seen to addresses some of the problems put forth in the proposal.
I do not disagree. I feel as if you may have misinterpreted my intent. I am not saying "this is so easy, look at how we can do it" - I am saying "here is an interesting constraint that might make this more approachable, and which could perhaps be used as the basis for additional discussion" I'm willing to brainstorm this, but again I am not proposing a concrete solution as much as attempting to provide a possible set of constraints for discussion. If that exercise shows that thia feature would too complex, that is a totally acceptable outcome in my opinion. Obviously we cannot use runtime code generation, I don't recall proposing that nor do I think it is necessitated by anything said above. Given that, here are some possible (not exhaustive or comprehensive) directions the compiler could choose: For
For
This is exactly what I attempted to do here. I wrote this at 1am on the last legs of a cup of coffee, and it seems I failed to consider some scenarios in my comment. A simple "How would this address X and Y" would have accomplished the same effect without this lecture at the end. |
To be clear, this is what the proposal says about this question:
The solution you suggest seem a variation on the second option here. |
I have read the issue thread and I still don't understand why adding just the ability to implement generic methods on concrete types (allow generic Is it about generic methods possibly fulfilling the requirement for a concrete method in an interface if the concrete method is a valid instantiation of the generic method? interface I {
Meth(string) string
}
type S struct {}
func (s *S) Meth[T any](v T) T {
return v
}
var _ I = &S{} // <- is this the blocking question? Because if in the future it starts fulfilling the requirement it would break resolution? I am new to Go and would really appreciate if someone could elucidate. |
@qm3ster I'm not sure what you are basing the question on. Is there a specific comment that made that claim, that you didn't understand? The FAQ says about this option:
This isn't about compatibility or keeping options open. |
That line is exactly about keeping options open to me. |
This is too literal an interpretation of the sentence. The solution declared unacceptable is adding generic methods while not having them participate in interface-satisfaction. Note that the premise of the section is that we add generic methods, that there are four options to treat their interaction with interfaces and that next sentence is
That is the status quo. What you describe is option 4. |
Wanted to suggest a way to "implement" the generic methods for interfaces but it looks like jpap has already suggested something similar. Oh well. Here're my thoughts on the matter.
This should prevent the compiler from using every type there is in the code for generating the required methods as it will be explained later.
5a. Every generic method in every interface should be turned into several non-generic methods with their type parameters substituted for every possible concrete types according to their constraints. For the
5b. For every type that has generic methods and that satisfies some interface the corresponding constraints from the interfaces' generic method type parameters should be used to generate all possible method variations for this type. So for the
So in the end we get the following:
Can't say much about the plugins for now. Considering all of their restrictions, the need to keep the zoo of toolchains and the fact that the |
Sorry to throw a beehive into the conversation given how little I know about the particulars, but could this gordian knot not be severed by adding something like C#'s extension methods to go? https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods Extension methods would have two issues:
However if all the feature is doing is improving generic ergonomics, the requirement that the extension methods live in the same package as the type it is extending and not create naming collisions can exist. You have the implementation benefits of free floating methods, and there are no issues with reflection, because the method is what it claims to be. The main issue is that it would be a syntax level change to correct a minor ergonomic problem. |
That's been proposed in several different ways before, including something like an Elixir/OCaml-type pipe operator so that regular functions could be reordered from inside-out to left-to-right, but they were all shot down. See #33361 and #49085 (comment), for example. |
The suggestion related to extension operators was shot down due to go not knowing how to manage them cross package, but as I mentioned above, it can be law that they need to occupy the same package |
I think that's way too restrictive, though. Take the example of iterators. Does that mean that all methods that can be chained to an slices.Collect(xiter.Something(seq.Filter(...)).Map(...)) I think the best approach is a pipe operator. It completely avoids cross-package issues and would simply be a better way to write certain things. The biggest problem, ignoring disagreements over whether it should even be added or not in the first place, is parsing it with semicolon insertion. Because of how semicolon insertion works, unless it got some kind of special casing, it would have to be seq |>
iter.Filter(...) |>
xiter.Something(...) |>
iter.Map(...) |>
slices.Collect() instead of the much nicer seq
|> iter.Filter(...)
|> xiter.Something(...)
|> iter.Map(...)
|> slices.Collect() |
Yeah, just like it is now. The proposal is to allow type parameters in methods. The author of iter.Seq would then be allowed to have type parameters in their methods. Problem solved. |
@CannibalVox I'm sorry but I'm having a hard time understanding what new syntax you're proposing, it doesn't seem obvious from the link you provided. |
There would need to be a way for free functions to identify that they are "extension methods" for a particular type. In the link provided, C# does this by having static methods declare their first argument as In doing this, an extension method This would allow the method |
@CannibalVox I think you are describing the ability to add a method to a type such that the method does not appear in an interface. That has been suggested many times in this issue, and we've consistently rejected it, because the point of methods is to satisfy interfaces. Apologies if I've misunderstood. |
I really hope we can reconsider that assumption. It ignores other practical reasons for defining methods on a type, including better API discoverability via code completion and method chaining, especially for builder types. (In short, +1 to option 4 from the FAQ.) |
I don't think the assumption can be challenged. Go does have a number of principles that have made its evolution slow, but on the other hand have succeeded in keeping the language small and coherent. Those are rare qualities and as such they should be preserved. And, yes, after hundreds of messages discussing the possibilities, the problem with methods with generic parameters always comes back to satisfying interfaces and how it doesn't seem to be possible without mandating a specific implementation strategy, which in turn is an unacceptable requirement. That amounts to saying that it doesn't seem to be solvable. The thing is... do the go designers think that it's unsolvable in principle, or just that a solution hasn't been found yet? If the former, I have nothing to add. But if the latter... wouldn't it be possible to waive the interface thing until a solution is found? I mean, rather than 'methods with generic parameters don't implement interfaces', 'methods with generic parameters don't implement interfaces for now'? I understand that there could be conflict if a method with a generic parameters happens to coincide with an actual method from an interface, but that doesn't look intractable. Can this be differentiated from a lax approach where everything goes and the consequences are worked out later? I don't know. If it can't, then ignore it. |
The FAQ is quite clear: "We do not anticipate that Go will ever add generic methods." |
Well, they're technically functions. |
@entonio I do not think that we can do something temporarily, as once we permit methods that are ignored by interfaces, we can't suddenly change those methods to satisfy interfaces. That would be a surprising change that would surely break some programs. |
I don't see why these would be methods. (the risk is to make type checking undecidable) On the other hand I can understand the appeal of scoped generic functions. Beyond this, the true issue is chaining. var r SomeStruct
r.Value = f(a, b)..h(e)..(A.g)(c).Format() (I prefer using double dots instead of a pipe operator. Also easier to write) What is evaluated first in the above? |
I think the double dots are confusing if there are any single dots in the same pipeline. A completely different operator with space separation makes the ordering more obvious, too. |
@DeedleFake that could be a choice but regardless of the symbol used, the question about the choice of precedence still remains? To be fair the question of precedence comes from knowing of the experience of the Dart's language designers (they have such an operator but it's not universally liked by the designers themselves). From a discussion we had:
But they have specific semantics. (understand sets as chaining methods of a same receiver) In a chain, we may want to be more explicit about which inputs of a function come from the chain. i.e. f(g)..h(.., .., a) where the double dot notation also acts as a placeholder, enforcing the arity. Then there are other considerations that actually explain why Dart made the choice it made (chaining methods). |
there is another way out,if we can declare the interface or struct(implement) can be part .if the answer is ok ,then we can define help method much more easier. // file collectionx/seq.go in other repo B(rely on A) in repo C(rely on A) seq means a simple interface with one method |
@ianlancetaylor, we can use a pragma to exclude the method from implementing any interface. In the interim, decree any method having type parameters and lacking the pragma as a compile-time error (e.g. "syntax error: method having type parameters must be excluded from interface implementation"). Later, once an implementation strategy is agreed upon, package authors can freely remove their pragmas to participate in interface conversions and type assertions as the need arises. I appreciate that nobody wants another pragma, but the above approach avoids surprises and future breaking changes. Being pragmatic about it, I'd rather have a small number of
|
Technically, the Go compiler already has support for the A certain large company uses Given that there is utility to |
I thought of the Dart operator when I saw your suggestion, but Dart's operator does something completely different. Dart calls it the cascade operator and it basically just calls a normal function, plus a few other things, but then returns the object instead of the method's return value. That way you can chain methods that weren't designed for it inside of a single expression, i.e. final m = new Map<int, int>();
m
..[3] = 2
..putIfAbsent(3, () => 1)
..[5] = 1
; A lot of languages, such as Elixir and Rust, also use the |
@DeedleFake yes that's what I mentioned in the previous post (it's edited). |
According to the Type parameters proposal, it is not allowed to define type parameters in methods.
This limitation prevents to define functional-like stream processing primitives, e.g.:
While I agree that these functional streams might be unefficient and Go is not designed to cover this kind of use cases, I would like to emphasize that Go adoption in stream processing pipelines (e.g. Kafka) is a fact. Allowing type parameters in methods would allow constructing DSLs that would greatly simplify some existing use cases.
Other potential use cases that would benefit from type paremeters in methods:
Assert(actual).ToBe(expected)
On(obj.Sum).WithArgs(7, 8).ThenReturn(15)
Edited by @ianlancetaylor to add: for a summary of why this has not been approved, please see https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#no-parameterized-methods .
The text was updated successfully, but these errors were encountered: