5 ms·
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
by latch 13y ago
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.
- deleted 13y ago[deleted]
- pcwalton 13y agoI 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.
- Retric 13y agoA 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.
- pcwalton 13y agoMaybe, 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.
- Retric 13y agoConsider 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.
- rdtsc 13y agoI 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.
- shasta 13y ago> 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.
- zak_mc_kracken 13y agoStructural 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.
- stormbrew 13y agoDuck 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.
- ori_b 13y agoPersonally, 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.
- eridius 13y agoThis 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.
- ori_b 13y agoI 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 :)