-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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: syntax for explicitly implementable interfaces #34996
Comments
Thanks for the detailed writeup. Given that there is already a way to address this problem, as you say, does this meet the "address an important issue for many people" listed at https://blog.golang.org/go2-here-we-come ? And, is providing another way to fix this problem worth the cost of two additional keywords? |
I would say that, at least in this case, the problem is the use of the type switch. In stead, there too, another interface should be used. I think that additionally to what you have, a Forwarder interface is needed, like this: type Forwarder interface {
ForwardMessage(Message)
}
type ServerMessage concept {
Message
Server() api.ServerID
Forwarder() Forwarder
}
type ClientMessage concept {
Message
Client() api.ClientID
Forwarder() Forwarder
}
func ForwardMessage(m messages.Message) {
m.Forwarder().Forward(m)
} Like this, the implementation of ForwardMessage becomes completely generic, and not dependant on the implementation details of Server and Client. |
It depends on how we estimate the possible number of programmers that would tempt to use Go interfaces for expressing "is a kind of" relationship to other types. I guess many people with background in traditional object-oriented languages like Java/C++ might still sometimes fall into the confusion. If we assume many people could potentially misuse Go interfaces in this way then clearly defined syntactic distinction between explicitly and implicitly fulfilled interfaces would highlight the difference.
True, the syntax was just an attempt to illustrate how it might look. Maybe there is a better way to express the same semantics, without introducing new keywords? |
Thanks for the suggestion. It seems to solve the problem in this simple case, but at the cost of reduced modularity. Now, every implementation of a message interface needs to know how to forward itself. Imagine we not only need to forward messages, but also do many other similar processing. One of the reasons to define the message interfaces was actually to decouple message processing from a particular message representation. |
Well you could of course implement the Forwarder on the Client and Server in stead, like that it doesn't need to be on the message itself. But let's leave the example aside. It seems to me what you want is not an explicitly implementable interface type, but a discriminated union. In that case, this issue is a duplicate of #19412. |
If I understood correctly, every concrete implementation would sill have to define
I'm not sure if sum types (discriminated unions) would work with both of the following type switches: switch m := m.(type) {
case messages.ClientMessage: // ...
case messages.ServerMessage: // ...
} switch m := m.(type) {
case messages.Request: // ...
case messages.Reply: // ...
} |
That's not true. You can embed a zero-width struct at the beginning another struct and it won't add any space to its representation. |
What is the benefit of this approach over "sealed interfaces" besides perhaps making a dependency more explicit in the code? |
@bradfitz The sentence refers to the suggestion of embedding the interface, not a structure. That was given to illustrate how the solution of sealed interfaces is error prone. |
@griesemer I tend to think that this proposal would primarily make it more clear that normal Go interfaces may not always provide the desired semantic in type assertions/switches when it comes to expressing "is a kind of" relationship. When one wants to express this, the language would provide a dedicated concept, clearly distinguished from Go's normal structurally-typed interfaces. Although the sealed interfaces give that semantic, it is not what one would learn with normal Go syntax. Honestly, I think it was a pure coincidence that I ever learned of this trick. Moreover, the approach with sealed interfaces may be error prone. Suppose, one sees the compiler complains something about unimplemented unexported methods. What one could do? Search it on the Internet, and arrive at the suggestion to embed the interface itself... |
I don't think you can blame a programmer's misunderstanding of what struct field embedding means on sealed interfaces. Making your "concept" explicit registration look like embedding will confuse that even more, people already seem to think embedding is OOP inheritance. |
@tv42 I'm not sure I understood you exactly.
Do you mean that it is a programmer's error to follow the dangerous suggestion and embed the interface itself in order to "implement" the unexported methods? In other words, do you mean that the programmer is supposed to fully understand the consequences of such embedding?
In the proposed syntax (which I don't fully like myself), there is no embedding. If a new type wants to implement a |
It looks like the official recommendation is to use the "Dummy Methods" alternative. As mentioned before:
|
@sergefdrv I'm not sure I understand what you mean by the need to have to define dummy methods for interfaces that are recursively embedded. But here's an example of the use of dummy methods (I'd call them "qualifying methods") in one of the oldest packages in the Go eco-system: go/ast, see the In short, I see zero advantage in adding a specific language mechanism that can be beautifully expressed with a mechanism we already have (methods), especially when the method names are chosen carefully. There is neither more safety, nor more readability, nor significantly simpler code. Nor is this something that people want to do so often that some form of syntactic sugar is warranted. Maybe using methods is "unusual" when coming from other languages, but a point of Go is to not add machinery for things that can be easily expressed with existing mechanisms. Yes, it needs to be learned (or found), and that is ok. |
@sergefdrv Sorry I must have been reading the wrong example and missed the "implements" keyword while writing the response. As for whether a programmer who chooses to use anonymous struct fields should understand what they mean, my vote is on a solid yes. I'd seen way too many attempts at "recreating" OOP inheritance like
that I consider your example of
to be very likely to just confuse people. As far as I understand, this whole thing started from a poorly-named I'm also weirded out by a "forwarder" that receives messages from clients and server over the same mechanism. Usually one has separate communication channels toward clients and servers, and knows which one is which one. So my take is "this doesn't look like typical Go code I've dealt with, and that makes me doubt the language should change for it". |
This opens the gates to explicit interface implementation and the coupling that comes along with it. If programmers from other traditional OOP languages see an I think it's important to consider how something will likely be used regardless of it's intended purpose, and whether or not that likely use is congruent with the philosophy of the language. Because Go does not include such keywords developers are encouraged to learn about implicit interfaces and the API designs that can be achieved with them. I think it is worth the effort to solve the problem outlined here using existing and more elegant methods. |
I tried to point out that if a new type wants to implement an interface with a qualifying method which in turn embeds another such interface etc. then the type has to implement all the qualifying methods along the "embedding chain".
Any type could indicate it implements an "explicit interface" using the same syntax (maybe not the one I tentatively suggested). Maybe it would not be easy to forbid external packages from implementing an "explicit interface". It would easily allow multiple interfaces, and I think it would play well with normal interfaces in general.
Having to define otherwise unused methods and choose the names carefully (thinking of CON, NUL, COM1, LPT1...) may not be synonymous to beauty 🙂
I love using Go interfaces so much for their orthogonality and flexibility. But when it comes to expressing "is a kind of" relationship in a generic code, I found others and me using Go interfaces to to that. When I realized the unexpected semantic of type switches, I wondered why the language syntax does not prevent from this misunderstanding? |
Exactly, the solution with sealed interfaces is tricky and might be confusing. This is why I submitted this proposal: to extend the language with ability to express this construction in a clear way.
The thing started from accidentally ambiguously-named methods. I wouldn't agree the naming is too bad/unusual. If we have a message of
Yes, this was an artificial, but possibly concise example. |
I share your concern about abusing this concept following OOP paradigm. Maybe type Reply interface<> { ... } type MyReply struct<messages.Reply> { ... } but it also looks
It might not be that dangerous if it will be just easier to use normal Go interfaces than this special form. What do you think? |
No, the confusion comes from the way you're trying to use embedded fields to signal is-a relationship. The original way encouraged by the Go core devs is just defining the marker methods, and has no such confusion. Don't generalize the confusion of your approach to all sealed interfaces.
Real examples would be better. |
I believe the confusion with unexported methods in general is very likely to happen. Search for "go unexported interface method" on the Internet, and you'll probably see this suggestion at the very top. What would be a proper way of implementing an interface with unexported methods (aka sealed interface) in another package? The qualifying methods solution was considered in the proposal as another alternative under the name of "Dummy Methods" (dummy in the sense of "not used as methods to invoke").
It was also given: please check this. |
By the way, is there a language that supports both structural and nominal typing to determine compatibility of user-defined types? |
That is not something I would call a sealed interface. The whole point of sealed interfaces, as used in Go, is that outsiders cannot implement them. |
Therefore, this is a likely decline. Leaving open for four weeks for final comments. -- for @golang/proposal-review |
It would be nice to see Go one day combines support for both structural and nominal way of matching generic types. (Could be a unique feature?) But anyway, thank you guys for spending your time and considering this proposal! |
No final comments. Closing. |
Abstract
This proposal suggests a dedicated syntax for certain uses of a
construction known as "sealed interface". This syntax requires another
type to explicitly mention an "interface" in order to implement the
latter.
Background
Consider, as an example, the following definitions from an imaginary
client-server application.
An intuitive way to use these definitions might be as follows.
However, the semantic of the type switch is different from the
intuitively expected one. In this case,
ForwardMessage
will invokeforwardClientMessage
given an instance ofmessages.Reply
, which isnot what the programmer will intuitively expect.
For more realistic example, please check this.
Proposal
The suggested solution is to extend the syntax with a special kind of
type, similar to interfaces. The new kind of type would behave the
same way as an ordinary interface except that other types would need
to explicitly specify it in their definition in order to implement it.
The example, using the suggested syntax, would look like follows.
Alternatives
Sealed Interfaces
There is a construction which allows to provide the desired semantics
of type switches/assertions relying on the current syntax of Go. This
construction employs interfaces with unexported methods, known as
"sealed interfaces".
To fulfill a sealed interface in another package, the new type needs
to explicitly embed a dedicated type that implements the interface's
unexported methods.
According to this approach, the example can be fixed as follows.
A proper way to fulfill the sealed interface in another package is to
define message types like follows.
As is apparent, this approach requires some annoying boilerplate code.
It is also prone to errors. For instance, consider the
suggestion.
Following this advice, one could end up mistakenly implementing
messages.Reply
as follows.Not only this implementation seems to embed an excessive pointer into
the structure, it also misses implementation of
Client
method.Although this implementation fulfills
messages.Reply
interface, aninvocation to
Client
method will result in nil-pointer dereferenceat run time.
Further restriction of the solution with sealed interfaces is that it
requires the new type to be a structure.
Dummy Methods
Another kludge could use dummy exported methods in the interfaces to
make unexpected type match less likely:
with implementation like this:
Notice however that in this case, in contrast to the proposed
solution, there is no automatic "promotion" of the dummy methods. This
means the programmer not only need to remember defining the dummy
methods for the most specific interfaces, but also for all the
interfaces recursively embedded in those, which is inconvenient and
error-prone.
Compatibility
The proposed syntax would introduce new keywords (like "concept" and
"implements"). This might break code using those words as type names.
The text was updated successfully, but these errors were encountered: