-
Notifications
You must be signed in to change notification settings - Fork 17.6k
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: make generic code clearer #36177
Comments
My third question is what the relation of contract and interface is. I think 'contract' is a generic concept of 'interface'. So, an interface is a contract. Generic code:
If we use a contract, it becomes the following code.
Polymorphic code:
|
My 4th question is how to be compatible with non generic code. For example. If I have a serialization package. Its code is the following.
How can I let it more generic? I think the simplest way is the following.
The following is a completed example.
|
Hello @xushiwei; thanks for the feedback! Here are (my) preliminary answers to your questions:
Your suggestion of writing [Node, Edge G]
func New(nodes []Node) *Graph[Node, Edge] {
...
} does read pretty nicely, but it doesn't match the function invocation (at least if type parameters are passed explicitly which is necessary here because the
And on the syntactic side:
|
This only change the notation, not the semantic. So we just call
to pass in the Edge type. |
I don't think contracts are syntactic sugar for interfaces. Suppose we don't change the semantic of "an interface is a value". We can't disassemble the following contract
into interfaces:
Why? Because here 'ReadObject' is not a normal method. It is a template method. If the interface 'ObjectReader' is legal, it is not a value. And more, contracts maybe need to support global functions. For example:
Of course, here 'XXX' can be 'io.ReadWriter'. But it also can be:
Now, 'FooBarable' is equivalent to 'io.ReadWriter'. Why need support global function? Let's see a previous example:
Here, the type 'T' isn't given a contract. but in fact its contract is:
So, If a function need call the global 'Read' function, we use the following equivalent contract:
|
The last question is: do we support operator overload? If we don't, I think a list of types is good, if we given some builtin constraints. For example, contracts.Integer, contracts.Float, contracts.Comparable, etc. Operator functions are very special. They exist in the global namespace, not in packages. So I agree not to support operator overload. The principle maybe is:
The following is an example.
|
We could borrow notes from SQL, as well as rust, and add a where clause for type parameters that need constraints. Consider the following example (with interface contstraints, due to its verbosity) func f(type P1 I1(P1), P2 I2(P1, P2), P3) (x P1, y P2) P3 We could potentially break that down to: func f(type P1, P2, P3) (x P1, y P2) P3 where
P1: I1(P1), P2: I2(P1, P2) Or a step further from this proposal: [P1, P2, P3]
func f(x P1, y P2) P3 where
P1: I1(P1), P2: I2(P1, P2) Overall its more verbose, but it might be more readable, especially with more type parameters |
One issue I see with the notation [T Readable, Archive io.Reader]
func ReadArray(arr []T, ar Archive) is that the call of ReadArray - when types are not inferred, doesn't match the declaration: In the declaration, the type parameters are declared before the function name, in the call they are passed after the function name. This is an aspect we have paid attention to in the past. I am not saying it cannot be changed, but it's something to be aware of. We have played with a lot of different notations for the type parameters, including using We've also been playing with more condensed forms of type parameter lists, for instance: func ReadArray(type T Readable, Archive io.Reader : arr []T, ar Archive) or func ReadArray(type T Readable, Archive io.Reader ; arr []T, ar Archive) This latter version would allow the following form (because of automatic semicolon insertion): func ReadArray(
type T Readable, Archive io.Reader
arr []T, ar Archive
) I personally like the latter because it's light-weight in the simple case, and allows for nice notation across multiple lines in the more complex case w/o extra overhead. Anyway, I prefer not debating the syntax for now because that's something we can change relatively easily once we have everything else nailed down. What we have now is workable to make progress. Regarding contracts: I am feeling very strongly that a contract is simply syntactic sugar for a set of interfaces. Specifically, given a general contract contract C(T1, T2, ... Tn) {
T1 m11()
T1 m12()
...
T2 m21()
T2 m22()
...
} etc. we have to represent the type bounds for each type parameter. The type bounds for type parameter type I1(type T1, T2, ... Tn) interface {
m11()
m12()
...
}
type I2(type T1, T2, ... Tn) interface {
m21()
m22()
...
}
... This is of course cumbersome to write but it is easy for the type-checker to create these interfaces from a contract. Everything that we can express in a contract we can express in an (extended) interface. For this to work, we also extend interfaces to have type lists, such as in (for example): type Adder(type T) interface {
Add(x T) T
type int, float32, complex64
} Whether we should allow such interfaces in places outside type parameter bounds is not clear yet, but it's very clear that this makes implementation straight-forward and explainable. More importantly, using interfaces as type bounds does fit with the type theory of generic type systems. Finally, considering contracts as syntactic sugar for interfaces also resolves the dilemma of what it means to mix contracts and interfaces. The dilemma disappears. A contract is simple a set of interfaces which (sometimes, but not always) may be less cumbersome to write down in form of a contract. I believe in the vast majority of cases, there will be exactly one type parameter, and in the vast majority of cases that type parameter has very simple constraints. Often it will be an interface such as Using parameterized interfaces also resolves the issue with what you call template methods - which is not a term we have defined for Go or know what it means. In refining our design at this point (syntax aside) it's really important to boil it down to the fine-grained semantics that permits us to implement a type checker that is understood and sound. We should not invent arbitrary mechanisms that are not fundamentally grounded in type theory. We are deliberately not trying to implement something like C++ templates, for that matter (as they cannot be type-checked seperately, w/o instantiation, which is the primary reason the resulting error messages are unreadable if there's a mistake somewhere). Regarding your last example (contract (As an aside, I just verified that package p
import "io"
func Foo (type T io.Reader) (r T)
func Bar (type T io.Writer) (w T)
func Example (type T io.ReadWriter) (t T) {
Foo(t)
Bar(t)
} type-checks fine with the current prototype, as expected.) |
@griesemer I really like the way you are currently thinking, especially the idea of extending interfaces to also have type lists and be parametrable. Like that, it becomes possible to group a few concrete types together without having to implement a marker method on them, or to use the native types directly through an interface. Please do consider allowing these extensions to interfaces everywhere, not just within the confines of generics. As for the syntax, I also like the |
I agee with everything @beoran has just said. In particular, I prefer the Although I personally think that contracts are a 'nicer' vehicle for expressing relationships between type parameters, the fact remains that in the majority cases only one type parameter will be required (or if two or more are needed there will be no relationship between them). Consequently, if the compiler is going to split contracts up into interfaces anyway, we might as well bite the bullet and go with explicit interfaces for everything. This will make the implementation simpler and, of course, will mean that a new keyword will not be required - just a new 'comparable' built-in. I also agree that it would be a good idea to not just confine interfaces with |
type Adder(type T) interface {
Add(x T) T
type int, float32, complex64
} Looks like it would be constraining instantiations of Was it meant to be the following: type Adder(type T) interface {
Add(x T) T
type T = int, float32, complex64
} It seems like that would be needed if there were two parameters with different underlying type constraints. |
I think myself it's correct as @griesemer had it: type Adder(type T) interface {
Add(x T) T
type int, float32, complex64
} It's saying that the Adder(T) interface can only be satisfied by types derived from int, float32 or complex64 which have a method with signature Add(x T) T. |
Incidentally, there's an example with two type parameters in @griesemer's prototype where: contract C(P1, P2) {
P1 m1(x P1)
P2 m2(x P1) P2
P2 int, float64
} disassembles into the two interfaces: type I1(type P1) interface {
m1(x P1)
}
type I2(type P1, P2) interface {
m2(x P1) P2
type int, float64
} i.e. one for each type parameter. So P1 has to satisfy I1 and P2 has to satisfy I2. Presumably, the corresponding contract for the Adder interface would be: contract AdderContract(T) {
T Add(x T) T
T int, float32, complex64
} |
Thanks, I had missed that when I skimmed the CL (I went straight to the examples). I do like the idea of using interfaces instead of contracts. If they can do everything that contracts can do, I'm not sure why there would also be contracts, though, even if sometimes they're more convenient. Is it just for comparability or is that also an interface like #27481? I'm not terribly enthusiastic about expressing type restrictions in interfaces, though. If you embed interfaces with type restrictions, wouldn't you need to take the intersection of the restrictions instead of their union? When that intersection is whittled down to ∅ doesn't that mean that no type satisfies it, making it distinct from not having a restriction at all? When using an interface as an interface instead of as a constraint, does it still work with the underlying types? |
If contracts were replaced entirely by interfaces, which I agree makes sense, then Although I'm on the fence about whether discriminated unions are a good idea for Go, they do seem popular and 'restricted interfaces' look like a convenient way of implementing them which would cause minimal disruption to the language. If that were done, then we'd need some syntax to indicate that the interface only applied to the listed types and not to types derived from them (I think I suggested If you then embedded one restricted interface in another, the listed types would be the union of the listed types in the two interfaces and those types would need to satisfy the union of the listed methods (if any). In theory this could result in an interface which no type could ever satisfy but we already have that possibility with generic contracts so I don't think it's a fatal problem. |
For a simple example, XXX just being io.ReadWriter is good. But if it is a complex example:
In this case, XXX is very complex:
When adding a new type for 'Read' function, we have to change both the 'Read' function and XXX contract to support this type. If we support global function, we just define XXX contract as following:
And now we just need to change the 'Read' function. |
In fact, contract C isn't the most general contract. It doesn't consider generic methods (ie. template methods). For example:
Of course, we may disassemble it into:
But the interface 'C' isn't a normal interface, because it has a generic method. So we can't define a variable of 'C':
|
It sounds good. Only a little flaw needs to consider. For example:
Because types A, B, C are declared in function parameters, It seems a little strange to use them in return types. |
Personally, I don't really regard that as a flaw. The type parameters are, in effect, a kind of 'input' parameter (you either need to supply them explicitly when you call the function or they are inferred) and I don't therefore think it is inappropriate to include them with the function parameters, albeit roped off into their own section by the final semi-colon. The type parameters may, of course, be used in the body of the function and would need to be if the result parameters are, or include, them. If the result parameters are named then they are effectively part of the body anyway i.e. they behave as if they were declared at the head of the body and initialized with their But what really matters here is that the type parameters are declared after the function name and before they are used and both the existing generics design and the It's only a suggestion anyway and as @griesemer intimated earlier we can worry about the syntax after the semantics have been sorted out. I've only commented on it at this stage as I wanted to express support for reducing the brackets needed which I think we all agree is a worthwhile objective. |
A number of people have commented that the syntax in the current design draft makes sense to them, as it clearly expresses that type parameters are parameters and type arguments are arguments. While certainly a number of people don't like the additional parameter list, I don't think there is universal agreement that it is bad. |
Sorry, I was being a bit presumptuous there :) I don't think it's bad either but, after playing around with it a bit, I'd concluded that including the type and function parameters in the same set of parentheses was more readable. In fact I was going to suggest this (using the vertical bar character |
I actually like the parentheses-rich syntax that was proposed in the draft. It does get a bit verbose at times though, and stuffs too much information into a single line. However, the reason that I liked it is because it truly shows what a type parameter is, it's a type as a parameter to the function. This proposal takes that away, as it takes away the idea that one is essentially (but not literally) calling a function that returns a function with the given types that we want to use. Also, |
Upon discussion with Go 2 proposal review committee, we would like to fold this issue into the general generics issue #15292. Please also feel free to add a link to this issue to https://golang.org/wiki/Go2GenericsFeedback. We don't think it will be helpful to keep multiple generics syntax issues open simultaneously, spreading the discussion to too many different places. -- for @golang/proposal-review |
Refer: https://blog.golang.org/why-generics
My first question is how to define a generic type or function.
I think this code is terrible. Go's function prototype is more complex than other languages.
Generics make it more terrible.
Maybe the following code is clearer.
My second question is how to define a contract.
Maybe the following code is clearer.
The following is a completed example.
The text was updated successfully, but these errors were encountered: