5 ms·
I've been programming for over 20 years primarily in languages in which this can nominally happen, and I think I've literally never seen it happen. I've been in
by jerf 7y ago
I've been programming for over 20 years primarily in languages in which this can nominally happen, and I think I've literally never seen it happen. I've been in situations where I expected a method and it was missing (e.g., Python code expecting a vaguely-file-like object to have seek, receives a socket), but that's the closest to this sort of thing I think I can say I've come. I've never had a case where I called a method that I expected to be one sort of thing, and got a method that was actually a completely different thing.
I used to worry about this sort of thing, but now I think that any language designer making a new language should just consider it a non-issue. If you happen to make it impossible for some other reason, sure, cool, I guess, but don't put even a little bit of effort into worrying about this case.
(Note I'm not saying it can't happen, or even be bad if it did. I'm sure it's happened to some people, and of that set, I'm sure there must be at least one person with a story of how it went really badly wrong somehow. I'm just saying that compared to the sorts of issues that affect every programmer in your language every day, worrying about this edge case is not productive. If this is your biggest problem in your language, or even fits into the top 100, please let me know about your surprisingly perfect language as I am very interested in using it.)
- throwaway894345 7y agoMy experience is the same. It's interesting to me that people are very concerned about Go's structural subtyping, but they generally have no qualms about passing functions around. Given that the contract for a function is the function signature, and the contract for an interface is [all function signatures and their associated names], surely the latter is less error prone?
- lmm 7y agoA function explicitly has no further semantics than its inputs and outputs. The receiving function shouldn't, and won't, assume anything about what the function it's passed does. Whereas when you're passed a bundle of named functions that (presumably) share state between them, it's very natural to assume that this implies relationships between how those functions will behave (even in the simplest examples, e.g. Java's Iterator has only two methods, but it also specifies a relationship between what those two methods return). And so it's very easy to write code that relies on those relationships, and then breaks when passed a bundle of named functions that does not conform to that relationship.
- jerf 7y ago"then breaks when passed a bundle of named functions that does not conform to that relationship." In that case, the fault lies with the thing that put unrelated functions together and passed them to a thing expecting them to be related. I'm not claiming that structural typing will somehow prevent programmers from deliberately writing wrong things, because I mean, what type system can make that promise? My point is that it is in my experience very rare for something expecting "something that can write bytes" to be accidentally passed "something that can write novels" or something equally unrelated, and then the world blows up in a bad way (that is, not just an exception thrown, because that's just a risk of dynamic languages, but actual bad things happening). Dynamic languages can at least still get what I said wrong; in Go it's even harder because as others have pointed out, Write([]byte) and Write(NovelInput) Novel still can't cross. But even in dynamic languages, this isn't a problem I had.
- lmm 7y agoNothing can go wrong with a single function like Write - but in that case you don't need a structural type, you can just pass a write function. As soon as you pass a bundle of two or more functions, you're implying a relationship between them, and it's then easy to accidentally violate that relationship - for instance, a structural type will almost always admit a mutable implementation where the relationship breaks down if the thing is mutated in between calls to the two functions.
- throwaway894345 7y agoLike I said elsewhere in the thread, you can have the same problem with callbacks: func receiver(next func() bool, getItem func() Foo) {} vs type FooIter interface { Next() bool; Item() Foo } func receiver(iter FooIter) {} You're more likely to pass the wrong set of callbacks than to pass the wrong FooIter implementation since the type system doesn't require the callbacks to be related to the same data nor named any certain way.
- lmm 7y ago
- lmm 7y agoA bug I've seen more than once is: a function is passed what it thinks is an immutable list, expects to be able to iterate over it repeatedly and get the same items in the same order, but what it actually has is a read-only view backed by a list that is being mutated by a parallel thread. Another bug I've seen repeatedly is a function that assumes it's passed a sorted list, but actually gets passed an unsorted list. The problem with structural typing isn't that completely unrelated things end up sharing a type. It's that you have no way to express the subtle distinctions between similar but different things, which are actually when having distinct types is most important.