-
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: Go 2: generic interface method receivers #54347
Comments
I think what you actually want is #43390 |
That proposal is different. It proposes enabling generics on method implementations, but not on interface methods. I actually do think interface methods should be able to be generic, separate from parameterized receiver types, but that seems like it might be a separate issue, and I tried to be careful to leave that out of this proposal. |
There is also conversation on #49085 |
That proposal seems equivalent to #43390. It deals with parameterized methods, and the author isn't asking for non-generic methods to match generic interface methods. Specifically, the author isn't asking for I don't think this proposal requires method values to be instantiated in the sense of |
Why not use generic interfaces? e.g. you can write type Number[N any] interface {
Add(N) N
}
func Add[N Number[N]](a, b N) N {
return a.Add(b)
} This seems to fill the same niche as what you propose, without adding any extra syntax. AFAICT all your examples can be rewritten in that way by moving the receiver type to a type parameter on the interface. It's also slightly more powerful, because it means you can have heterogenous types for the arguments. For example package main
import (
"fmt"
"time"
)
// Number is something we can add an A(rgument) to, to get a R(esult).
type Number[A, R any] interface {
Add(A) R
}
func Add[R any, N Number[A, R], A any](a N, b A) R {
// Order of type parameters is to make it possible to partially instantiate, until we get better inference.
return a.Add(b)
}
func main() {
fmt.Println(Add[time.Time](time.Now(), time.Hour))
} |
Some more thoughts: Would this be allowed and if so, what would it mean? type X interface {
R M1[R ~int](v R)
R M2[R ~string](v R)
} I would assume that any type implementing this would have to be required to implement the constraints of the receiver type of all methods in the interface. In that case, there would be no practical use for doing this kind of thing, as this interface can never be implemented. So maybe it should be disallowed - but I'm not sure we can practically do that, as it would require calculating whether the intersection of the constraints type sets are empty. So, perhaps it should just be disallowed to use different constraints on those receiver types - if one method specifies a receiver with a constraint, then all do and the constraints must be the same. The receiver is already guaranteed to be the same, of course. But then, that's syntactically redundant. Then there is the question of whether such interfaces should be allowed to be used as values - and if so, what that would mean. e.g. type Number interface {
N Add[N any](N) N
}
type MyInt int
func (n MyInt) Add(m MyInt) MyInt { return n+m }
type MyString string
func (n MyString) Add(m MyString) MyString { return n+m }
func main() {
// is this allowed?
var n Number = MyInt(0)
fmt.Println(n.Add(MyInt(1)))
// what about this?
n = MyString("Hello ")
fmt.Println(n.Add(MyString("World"))
// and about this?
if rand.Intn(2) == 1 {
n = MyInt(42)
}
fmt.Println(n.Add("World"))
} I guess we can't allow to use these as variables. But that seems kind of restrictive. Generic interfaces answer these questions:
So, I really think generic interfaces are the better solution for this problem. |
That's clever. I didn't know you could parameterize a constraint with a type parameter.
My old generics design didn't require explicit type parameter declarations, so no constraints were permitted: var _ interface { $A M($A) }
var _ interface { $A M() $A }
var _ interface { $A M($A) $A } With the current generics design, where type parameters and their constraints must be declared, the constraint would have to be required to be
Right, methods with receiver type variables can't be used on values that aren't arguments. Also from my generics design: var myInt = MyInt{1} // OK
var myFloat = MyFloat{2} // OK
var num1 Number = myInt // Makes sense
var num2 Number = myFloat // Makes sense
var num3 /* ??? */ = num1.Add(num2) // Invalid because the return type is unknown, because Number hides MyInt
// An interface method with a receiver type variable in a parameter can only be used when its interface is used as a constraint You could permit use of methods that don't have a receiver type variable, though: type Number interface {
N Add[N any](N) N
String() string
}
var myInt = MyInt{1} // OK
var myFloat = MyFloat{2} // OK
var num1 Number = myInt // Makes sense
var num2 Number = myFloat // Makes sense
fmt.Println(num1.String()) // Fine
fmt.Println(num1.Add(num2)) // Compile error Understanding when you can use which methods is a little complicated, and perhaps not worth the trouble.
It's no more restrictive than not allowing non-basic interface types for values. It's the same issue. I think I agree with you. Thanks for your thoughtful response. |
Author background
Would you consider yourself a novice, intermediate, or experienced Go programmer?
Experienced
What other languages do you have experience with?
JavaScript, HTML, CSS, Java, C, Haskell, Scheme, C#, Python, Ruby, PHP, Node
Related proposals
Has this idea, or one like it, been proposed before?
I don't think so
If so, how does this proposal differ?
N/A
Does this affect error handling?
No
If so, how does this differ from previous error handling proposals?
N/A
Is this about generics?
Yes
If so, how does this relate to the accepted design and other generics proposals?
It enables explicit generic receivers in interface methods. I don't believe the accepted generics design does anything similar.
Proposal
Generic interface method receivers
Non-generic operations, like addition (
+
) forint
, have concrete types for operands and results. Constraints can abstract out the concrete types for generic use:interface { ~int | ~uint }
.Non-generic functions, like
func(int, int) int
, have concrete types for parameters and results. Type variables can abstract out the concrete types for generic use:func[T any](T, T) T
.Non-generic method implementations, like
func (Foo) Bar(Foo) Foo
, have concrete types for the receiver, parameters, and results. Method implementations are abstracted by interface methods, but interface methods cannot use type variables to abstract concrete types in parameters or results, and match those to the receiver type. This means that interface methods are strictly less expressive of an abstraction than constraints for operations or type variables for functions. I propose we enable interface methods to specify generic receiver types to establish expressive parity between operations, functions, and interface methods.Specifying interface method receivers:
Now we can do:
This opens the door for a lot of things (like
interface { T Max[T any](T) T
), but I'll save those for another time.Go users who want their methods to be able to be abstracted with generics, e.g.
Built-in types and user-declared types should have parity in terms of features and expressiveness. It should be possible to describe built-in types and the types of their operations in terms of user-declared types and methods. Enabling this makes the language consistent and facilitates comprehension and learning the language. Since the equivalent of operations for user-declared types is methods, method receiver types should be able to be generic as well when abstracting over method types with interface methods. If int can have
int + int = int
, and be abstracted with constraints.Integer, then MyInt should be able to havefunc (MyInt) Add(MyInt) MyInt
, and be abstracted withtype Number interface { N Add[N any](N) N }
.You can now have interfaces that specify a generic receiver type, and use that receiver type variable in parameter and result types.
Yes.
Show example code before and after the change.
It expands the expressiveness of interfaces to match that of functions and operations.
No.
N/A.
N/A.
Costs
Easier, because operations could then be conceived of (whether true in actuality or not) as just generic methods.
Spec update. Implementation. go/ast update. Doc update.
Assuming all those use go/ast and go/types, then none after go/ast and go/types are updated. Perhaps if someone wrote their own type checker, then it would fail for the new cases enabled by this change.
Parse time would be minimal.
Type checking would be a little more involved, since it permits new constraints and matches that aren't already permitted. I imagine it's minimal.
Incorporating the runtime receiver type when checking whether a value implements an interface. Probably pretty cheap.
I think I've already sketched it as best as I can without being familiar with the implementation specifics of these things.
No.
The text was updated successfully, but these errors were encountered: