With implicit interfaces you're not tied to a specific interface, but rather the method(s). If you have a type that implements Close() error, your type can now be used anywhere (include other people's code) which accepts an io.Closer.
Add a MarshalText() (text []byte, err error) function and it can now be used anywhere that expects a encoding.TextMarshaler.
Furthermore, your type can be used in someone's else code which has defined its own MarshalTextCloser interface.
I believe this is what the OP means by emergent. You wrote a type that has two methods, within your own package called "goku.Sayan", and it accidentally satisfies the interface "picard.MarshalTextCloser" which you'll never know about. "Sayan" was never built to satisfy _any_ interface. You just needed a Close and MarshalText function, but none the less, "Sayan" satisfies 3 distinct interface (which could be represented by an unlimited number of names)
I realise accidental sounds dangerous, but the only real risk is that the methods don't do what you think they should. But I don't see how that's different from explicit interfaces; in both cases the intent is only implied by the name. Overall, as the OP says, it's quite powerful when dealing with simple 1 or 2 function interfaces. I think most Go developers see this naturally emerge in their own code: they favor small interfaces specifically for this type of re-use.
I think the chances that people independently developing two separate libraries will accidentally give their methods exactly the same name, with the same types of their arguments in the same order and with the same return value, allowing this "emergent behavior" are very low in practice. Rather, Close() and MarshalText() are really people implementing explicit, well-known interfaces defined in the standard library. MarshalText() is a good example of this—I would have named it something like Serialize() if not for the well-known, named interface I explicitly wanted to conform to.
The primary benefit that structural interfaces give you, IMO, is less typing. Which has plenty of value, don't get me wrong; it's awesome that Go has successfully reduced the amount of syntactic overhead needed to implement interfaces. But I think the benefits of it shouldn't be overstated.
Cowboys draw their guns; artists draw their pictures; an artist cowboy draws pictures and guns. It happens more than you think, and name hygiene is a big deal in programming languages.
The problem goes away with less ambiguous naming conventions (e.g. DrawGun() and DrawPicture()), but overloading is so gosh darn convenient, and thinking of unambiguous names is quite difficult. C# is really the only mainstream language that gets this right with explicit interface implementations.
Yes, that's a drawback to implicit interfaces I had forgotten: it fails to solve the cowboy-shape problem (and worse, there's no way to fix it in the future without introducing a whole new kind of interface: the language seems kind of stuck with this issue).
Agreed about C# multiple interface handling. I'd really like to see something like that added to Go at some point.
In fact it's probably not that far away from where Go is now. Go already has something very similar for implicit composition / mixins. Now, if you do something like this:
type Foo struct {
Bar
}
Then Foo automatically has the methods of Bar, e.g. Foo.bar() instead of Foo.Bar.bar() although the former is really just shorthand for the latter.
If you then have:
type Foo struct {
Bar
Baz
}
Then assuming that Baz also has the method bar() then you now need to be explicit about it and call Foo.Bar.bar() or Foo.Baz.bar() because the compiler doesn't know which one Foo.bar() means.
Of course, this is slightly different to interface disambiguation, but there's no reason why you couldn't have two function definitions with the same name and some added interface qualifier - the functions would then only be callable if the object is cast to an interface. In Go, casting to an interface is a bit like boxing, as it creates a separate vtable for that object's methods to match the interface ABI.
Hence, you could have say:
a := Foo(foo)
b := Bar(foo)
Now a has a vtable entry for Foo::foo() -> a.foo() and b has a vtable entry for Bar::foo() -> b.foo().
On the other hand, there would be no foo.foo() method unless one were declared separately without any interface qualifier.
I think this is more of a problem in languages that also encourage having deep class hierarchies with virtual dispatch as their main mechanism of polymorphism. It's in those languages that you tend to see argument-less functions like Draw() because the entire concept of what it is for that object to be drawn as well as where has been baked in to one large composite object.
In languages that place more emphasis on type deduction and composition it's much more likely you're calling draw to tell it what to draw on, and the aggregate type information once you introduce an argument or two is actually quite rich.
Which leaves you with things like Close(), which, let's be honest, we don't need 30 interfaces that all just have Close in them just because the base language didn't happen to include one. The concept is simple and relatively unambiguous and almost always has to do with some kind of disposal of resource.
That's just how natural language is; languages based on objects will suffer since that can't rely on full natural language power even if based on naturalistic concepts. Any duck typed system simply doesn't care much about name meaning at all.
This is not an issue in any kind of real code written by a human being. When would you ever pass a cowboy object into a screen painting function?
You'd never do this:
cb := cowboy.New()
screen.Paint(cb)
Anyone who wrote that code would have to be insane, and anyone reviewing the code would tell the person they were insane.
Also, would you not test your code? Like, at least run it and make sure it doesn't do crazy stuff? Maybe write some unit tests?
There can always be edge cases where functions don't work precisely as you expect, but that happens without interfaces, too. Pass an array into a sorting function and it turns out to sort by string length not alphabetically.... the programmer bears some small responsibility for actually understanding what the functions that he calls actually do.
I agree that it's generally not such a big problem. It may be useful to draw a cowboy on the screen, but it's more likely that confusion could arise with methods that do something related, not something entirely different.
For instance, in JDBC there is a PooledConnection.close() method and a Connection.close() method. Both Connection and PooledConnection are interfaces. They are semantically related but it's not a polymorphic relationship. PooledConnection does not extend Connection.
PooledConnection.close() must always close the actual physical connection to the database because PooledConnection is used by the connection pool itself. The Connection interface is used by the application and hence Connection.close() may close the connection or return it to a connection pool.
JDBC drivers usually come with implementations of both interfaces where the Connection implementation wraps an instance of a PooledConnection implementation. Arguably, being able to formally declare which interface a particular close method belongs to is beneficial in cases like this.
I think this is really a problem that exists beyond interfaces. For example, even if you had these two concrete types, and both had close methods, how would you know when to call one close versus another and what they do? This kind of confusion is a perpetual problem... and I don't think Go interfaces make it any worse, really.
I would know because the concrete types formally reference an interface, and the documentation of the interface would tell me more about the intended semantics.
You're right that simply having this formal reference doesn't solve all problems that could possibly arise. But there's one form of confusion that is much less likely to arise.
As is so often the case, more flexibility comes with more opportunity for screw-ups.
I can see you point in general about usually this not being a problem and I agree with you (I come from Python here so duck-typing is the name of the game if you wish). However I was trying to think where this might break (playing devil's advocate), I can think of specialized domains with lots of code referring to same terminology.
Ok like we have Close(). One could have Commit(), or MtxDeterminant() or Launch() things like that, where just a lots of people using same terminology in a domain are just bound to create collisions (on individual names!). Now high likely it is that a combination of those would be hit, not very sure on that.
A better example of Emergent behavior might be something that implements A and B it also incitements C if C is a subset of A and B.
Or if your refactoring and decide to split an interface so what was X is now Y and Z. Consider adding unkillable NPC's to a game. Rather than having special code to handle MOB's that you can't kill you remove HP's from the MOB interface and add a killable interface and now the type system helps you refactor your code by complaining when you try to harm something without HP's. At the same time you don't have to change any existing objects.
Maybe, but I'm not so sure about that either. Consider the two possibilities:
1. A and B were defined in separate libraries. In this case, the arguments about the likelihood of two types happening to define two methods with the same name and signature apply equally well to the likelihood of the two interfaces having a common subset at all.
2. A and B were defined in the same library. Here, I suspect most library authors in a language with explicit interfaces would notice this and factor out the common methods in A and B into a separate interface that A and B derive from. You might argue that the library writer could forget to do this, but I think it's not much more probable than the scenario in which A and B have no structural subset because the common functionality has a different names, or has a different return type, or has the arguments in a different order.
Consider cases where A is defined in a library that B depends on.
You might have Name and Location where Location is (latitude longitude altitude). Now you want to map things, while existing objects Trucks with Name and Location are easy you want also map stuff with a name and address. You can get a latitude and longitude from an address, but not altitude and adding meaningless altitude is IMO a bug waiting to happen.
I know what you mean; it doesn't [thankfully] happen for complex interfaces, but I maintain that it's useful (OP possibly overstates) as a composition of smaller interfaces. It's completely possible to write a type which satisfies http.ResponseWriter, both technically and in spirit, unintentionally while still being useful.
I've used it around a Stats interface which required a single method: Statistics() map[string]string and have been able to hook up otherwise independent code into a common logger. Speaking of loggers, the fact that the built-in log package doesn't expose an interface has always been an annoyance to me specifically because I can't build my own code and know that it'll satisfy a log.Logger interface in other projects.
> I think the chances that people independently developing two separate libraries will accidentally give their methods exactly the same name, with the same types of their arguments in the same order and with the same return value
> But I don't see how that's different from explicit interfaces; in both cases the intent is only implied by the name.
Go's approach is theoretically more error prone. It relies on names matching in a way that can happen by coincidence. The important part of explicit interfaces here is referencing a common declaration site, that could e.g. give informal requirements in the form of comments. I doubt Go's approach is particularly error prone in practice, though.
Personally, I think it would be better if interfaces were explicit, but the list of interfaces on a type was open, allowing you to add interfaces over any type anywhere, and not just in the place where the type is defined.
type foo = ...
and
impl hashable(foo) = ...
impl hashable(int) = ... // yep, adding to a builtin type
This is the way that Myrddin currently handles it.
This is similar to how Rust works. You can implement new traits (which are what Rust calls interfaces) on pre-existing types. The rule is that either the trait or the type must be defined in the current crate (e.g. in the current library).
This means that my library can define a new trait and implement it on, say, int. Or I can define a new type and implement a pre-existing trait on it. But it prevents me from implementing pre-existing traits on pre-existing types, which is important for the compiler to be able to answer the question "does type A implement trait B" without having the answer change when a new library is linked.
I do not have that restriction; Traits are instead scoped by module, so a trait implementation in one module is not necessarily available in another, unless it is exported explicitly. Now, on the other hand, I doubt Rust has the same kind of bugginess I have :)
Structural typing (sometimes referred to as "duck typing") has a few problems that make me unconvinced about the benefits.
First of all, I agree that the accidental argument is not very convincing (two developers writing a method Launch(), one that launches a football and another that launches nuclear missiles).
However, we lose two important things with implicit interfaces:
- Code readability. It becomes much harder for a human to interpret what types are actually implemented.
- More importantly, it severely limits the tooling available since the compiler has a lot less knowledge about the types you are dealing with. Automatic refactorings are all but impossible with structural typing.
Overall, I really don't see the harm in saying explicitly "My type is called Account and it implements Serializable and Entity" as opposed to me having to guess by reading all its methods and also having to remember which methods are necessary to be an Entity or a Serializable.
Duck typing and structural typing are not the same thing. In structural typing you are still making assertions about the type, in duck typing you are deliberately avoiding them. The opposite of structural typing is nominal typing, where in types are named and explicitly associated with the objects they represent.
Particularly, this is extremely wrong when talking about structural typing:
> - More importantly, it severely limits the tooling available since the compiler has a lot less knowledge about the types you are dealing with. Automatic refactorings are all but impossible with structural typing.
The compiler has as much information in structural typing as it does in nominal typing. The type of an object is determined at compile time either way, it's just that in structural typing Account implements Serializable and Entity because a subset of its methods match the definitions of Serializable and Entity, not because you said it does.
Add a MarshalText() (text []byte, err error) function and it can now be used anywhere that expects a encoding.TextMarshaler.
Furthermore, your type can be used in someone's else code which has defined its own MarshalTextCloser interface.
I believe this is what the OP means by emergent. You wrote a type that has two methods, within your own package called "goku.Sayan", and it accidentally satisfies the interface "picard.MarshalTextCloser" which you'll never know about. "Sayan" was never built to satisfy _any_ interface. You just needed a Close and MarshalText function, but none the less, "Sayan" satisfies 3 distinct interface (which could be represented by an unlimited number of names)
I realise accidental sounds dangerous, but the only real risk is that the methods don't do what you think they should. But I don't see how that's different from explicit interfaces; in both cases the intent is only implied by the name. Overall, as the OP says, it's quite powerful when dealing with simple 1 or 2 function interfaces. I think most Go developers see this naturally emerge in their own code: they favor small interfaces specifically for this type of re-use.