-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: spec: represent interfaces by their definition and not by package and name #8082
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
Comments
See also issue #8691. Status changed to Thinking. |
Comment 3 by [email protected]: While admittedly this issue comes up rarely in regular code, as a data point, I run into this issue quite a bit. I've built an RPC stub compiler that takes an API specified in my own language, compiles it, and then generates RPC stubs in different languages. Go is the core generated language. As a simplified example, let's say the user tells me to generate stubs for a RPC function "Print" that streams int32 from client to server, and streams strings from the server back to the client. Here's the Go API that I generate for the user on the client side. type PrintClientStream interface { // SendStream returns the send side of the client stream. SendStream() interface { Send(item int32) error // Places an item on the output stream Close() error // Indicates no more items will be sent } // RecvStream returns the receiver side of the client stream. RecvStream() interface { Advance() bool // Stages an item to be retrieved via Value Value() string // Returns the staged item Err() error // Returns errors } } Obviously the choice of "int32" and "string" are up to the user, as is the RPC function name. The reason SendStream() and RecvStream() return anonymous interfaces is because of this issue. A client may need to use two different services that define different APIs, but result in the same streaming types; client sends int32 and receives string. I'd like them to be able to write a single function that operates over the two different PrintClientStream interfaces. But if I named the return types from SendStream and RecvStream, they'd lose this ability. I also have a straw man proposal. I understand the concepts of type identity and assignability as defined in the language spec: http://golang.org/ref/spec#Properties_of_types_and_values I also appreciate the consistency of the current type identity rules wrt named types: a named and an unnamed type are always different. Whenever you see a "type" declaration in a source file, you know that a distinct type is being defined. Let's say we changed the language spec to say that type identity for interfaces ignores the name. I.e. interface names are simply a convenient shorthand, but for type identity purposes, it's as if every interface were anonymous. The rules for comparing these interfaces remains the same; it's an order-agnostic comparison of method names and their function types, and unexported methods in different packages are always different. I think this is a nice and consistent way to specify this feature. The main downside I see is that it's yet another special-case in the language spec. But interfaces are already special; we already have the special-case in our value assignability rules. <wishful thinking> If you haven't tuned out so far, here's a thought. Why wait until Go2 to incorporate this feature? My rationale: it seems unlikely that anyone was actually relying on the existing semantics. After all the compiler has ensured that all static type checks obey the existing rules, and we're simply relaxing those rules so that more cases are allowed. But for full disclosure, I do recognize that if we change this in Go1, existing code can notice a change in behavior. After all we provide type assertions in the language, as well as the reflect package. E.g. http://play.golang.org/p/OscKQCdDOL That's where my wishful thinking comes in. I can't think of a reasonable way someone would be using the existing semantics; my assertion is that if they're relying on something this subtle, their code deserves to be broken (and I'm only half-kidding). But I can see that if we strictly follow the Go 1 compatibility rules, we simply cannot change this feature, regardless of how much the code that relies on this deserves to be broken. Thoughts? |
This reminds me of the strawman to add typed objects to JavaScript, which considers two struct types equivalent iff they have the same fields (name and type) in the same order. |
I'm thinking main.Writer should be treated as different as io.Writer. Because WriterTo have different signature of argument. I prefer to use embed interface of io.Writer in this case. |
@mattn I want to be able to write a function that can receive any of the WriterTo interfaces. |
A quick note on this issue for anyone that is still watching it: Go "does the right thing" if the 2 inner interfaces are declared anonymous. That means the compiler and runtime already support the ability to do the right thing, it's just being overly strict if the interface has a name. Seems that a simple change, to ignore any given name when type checking 2 interfaces, would easily solve this issue. |
Here shows that if the interfaces are declared anonymous Go "does the right thing": http://play.golang.org/p/jpbd2325sj |
Removing Go2 label in light of use cases pointed out by #16209. Perhaps this (or some other solution) needs consideration before Go2. |
Interfaces are already special enough that I think this would be warranted (one could argue the same for function types, so that first-order functions aren't pinned to type name either). This may change/break existing programs, though. It's possible that some type assertions which fail now would succeed after this change. |
Not sure how. If the two interfaces have identical methods, then all type assertions between the two should be knowably and provably valid… I discussed this on the other bug #16209, in all, the worst thing possible is that two unnamed struct types might become assignable where they were not before, i.e.
But this seems like REALLY weird code… Otherwise, you have unnamed function types, which would accept either interface the same way when called, but then that's kind of exactly the desired behavior for context.Context moving from x/net/ into the mainline library. Finally, unnamed interfaces assign between identical interface definitions already… |
https://play.golang.org/p/NPkhXi0VuD Type assertions already go through fine: and the type switch tests for “implements interface” not “is actually typed this interface” As such, |
@puellanivis Under the proposal, this code would change behavior:
In Go 1, this panics because If the proposal is accepted, the code would succeed because |
A) don't use pointers to interfaces… it's pretty much redundant, and almost certainly not what you want to do. B) your code panics even if you attempt: https://play.golang.org/p/alO15c93b- And it STILL fails even if you change the var decl to And then change the |
That may be, but they're still part of Go 1 and they do have valid uses.
That's because
|
Quoting the Go1 guarantee:
As a subset of all code accepted by Go1 any code that compiles and runs without panic will, unchanged continue to compile and run without panic if this proposal were implemented. Next, according to specs:
Note that Go1 only guarantees for programs that “run correctly.” And by spec, having a type assertion that will knowably at compile-time panic a type-assertion is, by definition, not a correct program. Thus, the only case we're REALLY dealing with here is: First, let's set aside that this code is wrong wrong wrong, and the wrongiest wrong that ever wronged.
Thus, the code will actually start working in a way that is guaranteed to work correctly. Because whether Namely, there is no code where treating type *U1 and *U2 as identical could possibly cause “broken” behavior. After all, they were already “meta-semantically” identical… So, as noted, this actually makes MORE code correct, and is a strict super-set of all currently correct code.
I just have to say, breaking this out into what it is actually saying; this is making a variable u, which is an untyped empty interface, which is set with the concrete type to an empty interface, and a value of concrete value of nil. This code kinda made me throw up a little in my mouth… |
Your interpretation of "correct" here is stricter than intended: correct Go programs are allowed to panic. Happy to discuss further if you disagree, but let's move it to golang-dev. It's tangential to this proposal. |
I want to add that if this feature was implemented, it would allow a whole new class of packages/libraries to require no imports. Let's say you follow the paradigm of receiving an interface and returning an implementation when designing your functions and methods. Now you are writing package z which should be compatible to the interface A in package x and B in package y. ATM you would need to reference x.A and y.B directly and therefor need to import packages x and y. Your package becomes dependent of them. With the proposed change you could simply copy the definitions of the interfaces A and B to your library z and use them as parameter types or embed them to structs without having to import x or y. This way your package z has no dependencies and the users of your package z might combine it with x and y and at a later point swap x or y out and replace them with compatible versions without breaking any dependency of z. All in all this could lead to a better and more decoupled library ecosystem based on compatible interfaces. This is the most important argument for the change IMHO. |
This isn't a matter of “correct programs aren't allowed to panic” it's that a type assertion that is KNOWN to always panic, is not a correct program. But as well…
Thus, there is no possible correct code that can depend upon a type assert and only a type assert throwing a panic. Any deferred function recovering run-time panics must by necessity handle all possible run-time panics. (like instead of let's say nil-pointer dereference) Otherwise, they are—by necessity—depending upon unspecified behavior. |
@neild There may possibly be minor technical differences (e.g., how an interface is printed, either by name or by its literal, depending on how this would be implemented), but my understanding of this proposal is that if we would write every interface type definition as a alias type declaration, we'd get the same effect. The problem is of course that you can't do that with pre-existing code that you don't control but may depend on. Interfaces already behave essentially like the proposal suggests for operations such as assignments, type assertions, equality operations, etc. because there we have special rules in place. But as @ianlancetaylor pointed out, this proposal is really affecting the definition of type identity for interfaces, which comes into play when we have other (non-interface) types composed of interfaces: A function type |
@griesemer wouldn't this also have to extend to interfaces such as
which cannot be expressed with type aliases? |
@jimmyfrasche I'm not sure I understand the question. Do you mean something like
except that we wouldn't have to write the |
This compiles today (https://play.golang.org/p/cnLlSHpwRz): type I interface {
SetFrom(I)
} As an alias, it fails to compile with type I = Interface {
SetFrom(I)
} So that's one of the “minor technical differences”: interfaces can be recursive, but aliases (at the moment) cannot. |
Upon further reflection, the fact that aliases cannot be recursive, combined with the fact that mutually-assignable interface types are not identical, is probably going to be a significant headache for me. I'm trying to write a code generator that can wrap C++ APIs as Go APIs, and I had planned to use aliases of interface types to represent template instantiations (which will need to be defined in every Go package that wraps a C++ API that instantiates the template). I have to use type aliases rather than defined interface types so that the instantiated interfaces are mutually-assignable with the same instantiations in other wrapper packages, even if they include methods that refer to other template instantiations. (For example, consider Unfortunately, the prohibition on recursive aliases means that I can't use that approach to wrap any C++ member function that refers to its own type. Notably, that includes all assignment operators. (On the other hand, my problem could be addressed by allowing recursive aliases, which would be strictly compatible with Go 1 but perhaps more difficult to implement in practice.) |
@bcmills Let's not digress from the proposal at hand. Yes, the above example is currently not accepted, but it's not clear if that's "just" and implementation issue. At least I don't see why it couldn't be accepted. |
Right, I don't mean to digress. My point is that there are several possible solutions to the problem I have, and this proposal is one of them. (Accepting mutually-recursive aliases would be another. Allowing aliases of struct types to define methods would be a third, although that seems less likely to happen.) |
When it comes to recursive interface definitions, I would propose, that
and
would result in the same (nameless) internal representation. |
Let's go exponential? |
@metakeule The internal representation doesn't have to be the same, or nameless; the implementation may choose whatever representation is suitable. The point of this issue is that the rules for identity for interfaces would have to be changed in the spec. |
@griesemer Yes, you are right. I chose "internal representation" as a helper to illustrate the point, that when checking for identity there would be some representation that is not part of the language but has some meaning or checks for "self-ness" if that is a word; an argument that is the surrounding interface itself would not be considered the interface name, but this special placeholder "self". |
I'd still love for this to be fixed for Go 2! |
This proposal seems to be missing the |
I came across this problem recently when I am trying to implement marshaling/unmarshaling objects to/from key-value stores. I think solution to the original problem in comment 1 would be good to have in Go 2. Below is my interface design for marshaling objects into key-value stores. Any other solutions to my problem are also much appreciated. Thanks. The Problem - Cyclic Dependencies between interfaces wouldn't compile.
My Solution 1 - Break the cycle using expanded anonymous interface.
Problem 2 with Solution 1 - Following TreeEncoder doesn't compile because
Solution for Problem 2 - This works. Since number of encoder types would be far fewer than number of object types that must implement marshaling methods, I am going with this for now.
|
The text was updated successfully, but these errors were encountered: