3 ms·
You're contradicting yourself a bit here. Implementing an interface is just a special case of inheritance. The fundamental reason why some languages make a for
by mumblemumble 6y ago
You're contradicting yourself a bit here. Implementing an interface is just a special case of inheritance.
The fundamental reason why some languages make a formal distinction between interfaces and purely abstract base classes is that using the "interface" keyword is effectively signing a contract that says you will not do this one thing (implementing virtual methods), and in return the compiler will allow that base type to participate in multiple inheritance.
edit:
I should also mention that it's entirely possible to violate LSP using only interface inheritance. Java's collections do this: Collections.unmodifiableList() returns a List<T> that can't be modified. But List<T> is rotten with methods for mutating the list. So what happens if you try to mutate one of these? You get a rather surprising exception.
I suppose you could argue that this isn't technically a violation of LSP, because the JavaDoc for List<T>::add says that it might throw UnsupportedOperationException. But UnsupportedOperationException is a RuntimeException, not a checked exception, and it's rare for Java developers to make a habit of handling UnsupportedOperationException when mutating collections. So I'd argue that it's only a part of the method's spec in the most pedantic of ways. IMO, the more pragmatic perspective is that creating a subtype that inherits a whole bunch of interface methods that it has no intention of implementing is just about as clear-cut a violation of LSP as you can get.
- rovolo 6y agoYep, the read-only List<T> implementations definitely feel like a violation. For the benefit of people using older languages, I'd like to point out that newer languages tend to split collection interfaces into ReadOnly and ReadWrite variants, where ReadWrite subclasses ReadOnly. This means that a mutable list can be assigned to a read-only list variable, but a read-only list can't be assigned to a read-write list variable (because you can't write to an unmodifiable collection).
- nybble41 6y agoThere is a third variant which should also be considered: Immutable. It offers exactly the same interfaces as ReadOnly but with the additional guarantee that not only can you not mutate the object through that reference, no one else can either. Enumerating an Immutable collection or getting the item at a particular index multiple times is certain to always give you the same result, which allows simpler code and (with compiler support) better optimization. This would be superfluous in Rust since the borrow checker ensures that no one else can mutate data that you have a reference to, but most other languages with mutation would benefit. However, the only mutable-by-default language I am aware of which directly distinguishes between read-only ("const") references and immutable data is D.
- hinkley 6y agoI read an interview somewhere with Bloch where he said he became aware of this problem while architecting the collections api, but thought the alternative had too many interfaces and would confuse people. He ended up repeating the mistake made with Arrays in the JVM. I sat down and did the math once while contemplating writing my own collections API, and found a much smaller representation based on the observation that the read-only parts of some collections are equivalent, and hence don't deserve a separate type. This got me down to about 12% more interfaces, which I thought was probably worth it for essentially splitting everything in two, especially since it reduces the amount of Type Variance the user has to deal with (especially before they added inference to type declarations). For collections, most of the substitution problem is around shared state, and the differences between readers and writers. So now I'm thinking about borrow semantics in for instance Rust. When you are handed an object reference, you are only getting part of the interface for that object, unless you have made arrangements for the caller to give you control of the internal state of that object. Most, but certainly not 100%, of the covariance that I see in interfaces revolves around updating state. For instance you don't have to make a contains() function covariant to fulfill LSP. Because obviously the answer to "does this set of integers contain the string 'foo'" is no, it doesn't. Meanwhile put() or update() do care a great deal about the inputs. I'm wondering now if you could completely (or at least by default) fold borrow semantics straight into your type system and dispense with most or all of the out of band syntax that Rust requires...
- dllthomas 6y ago> I'm wondering now if you could completely (or at least by default) fold borrow semantics straight into your type system and dispense with most or all of the out of band syntax that Rust requires... IIUC, that's what "affine types" are about. See https://en.wikipedia.org/wiki/Substructural_type_system https://en.wikipedia.org/wiki/Substructural_type_system
- dmurray 6y agoCan you turn an unchecked exception into a checked exception with a rule in your linter? Or would it just be too painful to catch UnsupportedOperationException everywhere if you did that?
- mumblemumble 6y agoYou could, but I'd argue it's a dodgy thing to do. In general, the idea behind RuntimeException is that they're not checked because they're preventable errors that you should be able to avoid having in the first place, and it's therefore unwise to try to recover from them because they indicate that you've got a bug and the application may be in an untrustworthy state. Granted, that idea itself gets a bit dodgy. In the case of Collection<T>, for example, an isReadOnly method was not supplied, so we haven't really been given any good way to tell ahead of time if a particular collection instance supports mutation.
- dllthomas 6y agoAnd even then, the linter will only help if it's your code generating the exception.