6 ms·
I'm not sold the evidence is there to show inheritance is a good idea - it basically says that constructors, data storage and interfaces need to be intertwined.
by roenxi 1y ago
I'm not sold the evidence is there to show inheritance is a good idea - it basically says that constructors, data storage and interfaces need to be intertwined. That isn't a very powerful abstraction, because they don't need to be and there isn't an obvious advantage from doing so over picking up the concepts separately as required. And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things. Weird edge cases like HTTP over non-TCP protocols or rendering without screens start throwing spanners into a tree of assumptions that never needed to be made and pull the truth-in-code away from the truth-in-world.
All that makes a lot of sense if it was introduced as a performance hack rather than a thoughtfully designed concept.
- kragen 1y agoI'm on the fence about inheritance myself; I often regret having used it, and I never regret having not used it. On the other hand, it's awfully expedient. I designed and implemented a programming language called Bicicleta whose only argument-passing mechanism is inheritance, and I'm not sure that was a bad idea. The object-oriented part of OCaml, by the way, has inheritance that's entirely orthogonal to interfaces, which in OCaml are static types. Languages like Smalltalk and, for the most part, Python don't have interfaces at all.
- echelon 1y agoRigid, "family tree"-style inheritance as in classical OOP is pretty much garbage. "A cow is a mammal is an animal" is largely useless for the day to day work we do except in extremely well-planned, large and elaborate ontologies -- something you typically only see in highly structured software like windowing systems. It just isn't useful for the majority of our work. "Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules [1]. You can bolt behaviors and their types onto structures at will. This is how OO should be. I put together a visual argument on another thread on HN a few weeks ago: https://imgur.com/a/class-inheritance-vs-traits-oop-isnt-bad-inheritance-is-bad-bmdUV3H https://imgur.com/a/class-inheritance-vs-traits-oop-isnt-bad... [1] Though if you want rules on bounds and associated types, you can have them.
- DaiPlusPlus 1y ago> "Trait/Typeclass"-style compositional inheritance as in Rust and Haskell is sublime. It's similar to Java interfaces in terms of flexibility, and it doesn't enforce hierarchical rules. Yes-and-no. Interfaces still participate in inheritance hierarchies (`interface Bar extends Foo`), and that's in a way that prohibits removing/subtracting type members (so interfaces are not in any way a substitute for mixins). Composition (of interfaces) can be used instead of `extends`, but then you lose guarantees of reference-identity - oh, and only reference-types can implement interfaces which makes interfaces impractical for scalars and unusable in a zero-heap-alloc program. Interface-types can only expose virtual members: no public fields - which seems silly to me because a vtable-like mechanism could be used to allow raw pointer access to fields via interfaces, but I digress: so many of these limitations (or unneeded functionality) are consequences of the JVM/CLR's design decisions which won't change in my lifetime. Rust-style traits are an overall improvement, yes - but (as far as my limited Rust experience tells me) there's no succinct way to tell the compiler to delegate the implementation of a trait to some composed type: I found myself needing to write an unexpectedly large amount of forwarding methods by hand (so I hope that Rust is better than this and that I was just doing Rust the-completely-wrong-way). Also, oblig: https://boxbase.org/entries/2020/aug/3/case-against-oop/ https://boxbase.org/entries/2020/aug/3/case-against-oop/
- int_19h 1y agoHow are interfaces with ability to provide default implementations for members (which both C# and Java allow today) not a substitute for mixins? "Only reference types can implement interfaces" is simply not true in C#. Not only can structs implement them, but they can also be used through the interface without boxing (via generics).
- AstralStorm 1y agoThey just cannot access any field in the class itself. Which means... the default implementations fail to actually implement anything but the simplest methods or you need to expose API-private things via getters violating open-closed principle. (If you merge multiple interfaces, the implementations of the methods have to match. You end up with even more special getters for each one sometimes.)
- galbar 1y agoPython has Protocols. They work like Go interfaces
- kragen 1y agoSure, but for the most part people don't use them, because you don't have to; Python method calls are always potentially polymorphic, unlike Golang method calls.
- igouy 1y ago1992 "Interfaces and Specifications for the Smalltalk Collection Classes" https://dl.acm.org/doi/pdf/10.1145/141936.141938 https://dl.acm.org/doi/pdf/10.1145/141936.141938
- kragen 1y agoVery interesting work! It is an attempt to extract the interfaces that were in the minds of the implementors of the Smalltalk-80 system's collection classes, but which couldn't be expressed in the language itself, because it has no interface construct. That's what I meant by "Languages like Smalltalk (...) don't have interfaces at all."
- igouy 1y agoDon't have manifest types and don't have manifest interfaces. Someone has already referenced "Adding Dynamic Interfaces to Smalltalk" [0] and looking back there doesn't seem to be any kind of demonstration that use of interfaces makes software faster to develop or less error prone or... [1] [0] https://www.jot.fm/issues/issue_2002_05/article1/ [1] https://www.cs.utexas.edu/~wcook/papers/OOPSLA89/interfaces.pdf
- kragen 1y agoUnfortunately, in general, there is not much convincing proof that one way of developing software is better than another in such ways.
- igouy 1y agoAnd even if there were: "We are not finders of fact. We are tellers of story. You base the story on the evidence, no? No! We base the evidence on the story. We prove what helps us, and we disprove what hurts us. Whoever tells the best story goes home with the cash-in prizes." (The Good Fight, season 3 episode 2. 30 - 31 minutes. CBS All Access 2019.)
- dataflow 1y ago> And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things. "This doesn't represent the fundamental truth" does not imply "this has little value". Your navigation software likely doesn't account for cars passing each other on the road either -- or probably red lights for that matter -- and yet it's still pretty damn useful. The sweet spot is problem- and model-dependent.
- tippytippytango 1y agoIf you pretend/imagine it was intentional, and insightful, you've created a nerd trap for amateur ontologists. Some of which decide to become professional ontologists and sell books on objected oriented design.
- tobr 1y agoYour lovely typo there makes me realize how often I’ve had to deal with objection-oriented programming.
- osigurdson 1y agoYeah ya... everyone likes to go on and on about how inheritance is the root of all evil and if you just don't use it, everything will be fine. Sorry, it won't be fine. Your software will still be a mess unless it is small and written three times by the same person who knows what they are doing. The bottom line is, no one ever really used inheritance that much anyway (other than smart people trying to outsmart themselves). People created AbstractFactoryFactoryBuilders not because they wanted to, but because "books" said to do stuff like and people were just signaling to the tribe. So now, we are now all signaling to the new tribe that "inheritance is bad" even though we proudly created multiple AFFs in the past. Not very original in my opinion since Go and Rust don't have inheritance. The bottom line is, most people don't have any original opinions at all and are just going with whatever seems to be popular.
- josephg 1y ago> The bottom line is, no one ever really used inheritance that much anyway If you think that, you have no idea how much horrible code is out there. Especially in enterprise land, where deadlines are set by people who get paid by the hour. I once worked on a java project which had a method - call a method - call a method - call a method and so on. Usually, the calls were via some abstract interface with a single implementor, making it hard to figure out what was even being executed. But if you kept at it, there were 19 layers before the chain of methods did anything other than call the next one. There was a separate parallel path of methods that also went 19 layers deep for cleaning up. But if you follow it all the way down, it turns out the final method was empty. 19 methods + adjacent interface methods all for a no-op. > The bottom line is, most people don't have any original opinions at all and are just going with whatever seems to be popular. Most people go with the crowd. But there's a reason the crowd is moving against inheritance. The reason is that inheritance is almost always a bad idea in practice. And more and more smart people talking about it are slowly moving sentiment. Bit by bit, we're finally starting to win the fight against people who think pointless abstraction will make their software better. Thank goodness - I've been shouting this stuff from the rooftops for 15+ years at this point.
- HdS84 1y ago
- ninetyninenine 1y agoIt is a good idea because it's the most fundamental idea. You have two objects. A and B. How do you merge the two objects? A + B? The most straight forward way is inheritance. The idea is fundamental. The reason why it's not practical has more to do with human nature and the limitations of our capabilities in handling complexity then it has to do with the concept of inheritance itself. Literally think about it. How else do you merge two structs if not using inheritance? The idea that inheritance is not fundamental and is wrong in nature is in itself mistaken.
- josephg 1y ago> Literally think about it. How else do you merge two structs if not using inheritance? What? Using multiple inheritence? That's one of the worst ideas I've ever seen in all of computer science. You can't just glue two arbitrary classes together and expect their invariants to somehow hold true. Even if they do, what happens when both classes implement a method or field with the same name? Bugs. You get bugs. I've been programming for 30 years and I've still never seen an example of multiple inheritance that hasn't eventually become a source of regret. The way to merge two structs is via composition: struct C { a: A, b: B, } If you want to expose methods from A or B, either wrap the methods or make the a or b fields public / protected and let callers call c.a.foo(). Don't take my word for it, here's google's C++ style guide[1] > Composition is often more appropriate than inheritance. > Multiple inheritance is especially problematic, because it often imposes a higher performance overhead (in fact, the performance drop from single inheritance to multiple inheritance can often be greater than the performance drop from ordinary to virtual dispatch), and because it risks leading to "diamond" inheritance patterns, which are prone to ambiguity, confusion, and outright bugs. > Multiple inheritance is permitted, but multiple implementation inheritance is strongly discouraged. [1] https://google.github.io/styleguide/cppguide.html#Inheritance https://google.github.io/styleguide/cppguide.html#Inheritanc...
- ninetyninenine 1y ago>What? Using multiple inheritence? You just threw this in out of nowhere. I didn't mention anything about "multiple" inheritance. Just inheritance which by default people usually mean single inheritance. That being said multiple inheritance is equivalent to single inheritance of 3 objects. The only problem is because two objects are on the same level it's hard to know which property overrides which. With a single chain of inheritance the parent always overrides the child. But with two parents, we don't know which parent overrides which parent. That's it. But assume there are 3 objects with distinct properties. A -> B -> C would be equivalent to A -> C <- B. They are isomorphic. Merging distinct objects with distinct properties is commutative which makes inheritance of distinct objects commutative. C -> B -> A == A -> B -> C >I've been programming for 30 years and I've still never seen an example of multiple inheritance that hasn't eventually become a source of regret. Don't ever tell me that programming for 30 years is a reason for being correct. It's not. In fact you can be doing it for 30 years and be completely and utterly wrong. Then the 30 years of experience is more of a marker of your intelligence. The point is YOU are NOT understanding WHAT i am saying. Read what I wrote. The problem with inheritance has to do with human capability. We can't handle the complexity that arises from using it extensively. But fundamentally there's no OTHER SIMPLER way to merge two objects without resorting to complex nesting. Think about it. You have two classes A and B and both classes have 90% of their properties shared. What is the most fundamental way of minimizing code reuse? Inheritance. That's it. Say you have two structs. The structs contain redundant properties. HOW do you define one struct in terms of the other? There's no simpler way then inheritance. >> Composition is often more appropriate than inheritance. You can use composition but that's literally the same thing but wierder, where instead of identical properties overriding other properties you duplicate the properties via nesting. So inheritance A = {a, b}, C = {a1}, A -> C = {a1, b} Composition: A = {a, b}, C = {a1}, C(A) = {a1, {a, b}} That's it. It's just two arbitrary rules for merging data. If you have been programming for 30 years you tell me how to fit this requirement with the most minimal code: given this: A = {a, b, c, d} I want to create this: B = {a, b, c, d, e} But I don't want to rewrite a, b, c, d multiple times. What's the best way to define B while reusing code? Inheritance. Like I said the problem with inheritance is not the concept itself. It is human nature or our incapability of DEALING with the complexity that arises from it. The issue is the coupling is two tight so you make changes in one place it creates an unexpected change in another place. Our brains cannot handle the complexity. The idea itself is fundamental not stupid. It's the human brain that is too stupid to handle the emergent complexity. Also I don't give two flying shits about google style guides after the fiasco with golang error handling. They could've done a better job.
- codr7 1y agoBut they're not building trees, that's how inheritance is mostly used today. After reading this, I'm thinking that intrusive lists is the one use of inheritance in C++ that makes any sense.
- josephg 1y agoI'd still generally prefer intrusive lists to be done via composition. I've seen plenty of intrusive lists where each item was a member of multiple lists at the same time - which is quite hard to do if you need to inherit from an intrusive list element superclass.
- codr7 1y agometoo, but how do you pull that off in C++? How do you get back from node to containing value? Multiple inheritance, possible but you'd have to jump some hoops to disambiguate since you're dealing with multiple copies of the same base class.
- josephg 1y agoComposition. Sorry about the syntax - its been awhile since I wrote C++. class<T> IntrusiveListNode { T* next, T* prev } class SomeObj { IntrusiveListNode<SomeObj> list_foo; IntrusiveListNode<SomeObj> list_bar; }
- codr7 1y agoAh, of course; templates and linking the full object. Thanks for the update, my C++ is pretty rusty. But it brings up another problem for me; when iterating a list like that, how do you know which of the links to follow? I get that you can do it manually step by step, but if you wanted to say write an iterator. Member pointers?
- ninetyninenine 1y ago> a tree probably doesn't represent the fundamental truth of things It does. Trees appear in nature all the time. It's the basis of human society, evolution and many things. Most of programming moves towards practicality rather then fundamental truth. That's why you get languages like golang which are ugly but practical.
- josephg 1y agoBotanical trees appearing in nature don't make them "the fundamental truth of things". And in what way are trees the basis of human society? Thats such a strange claim. Are you talking about family trees? Because they're actually directed acyclic graphs. Even if you want to claim that trees are a common data structure, that doesn't mean they're appropriate in any specific case. Should we therefore arrange all websites in a tree? Should relational databases be converted to trees, because "they're the basis of human society"? What tosh. Programming moves toward practicality because software is created to do work. Taxonomies are entirely and completely worthless. The classic Animal -> Mammal -> Cat example for inheritance is a fascinating ontology, and an entirely worthless piece of software.
- inglor_cz 1y ago"Taxonomies are entirely and completely worthless." Hard disagree. Knowing that AES and Twofish are block ciphers is useful when dealing with cryptography. Many categories of algorithms and objects are naturally taxonomic. Even HTML+CSS has (messy) inheritance.
- josephg 1y agoI may have overstated the claim in that sentence. More accurately, I think taxonomies aren't, themselves, useful. If they don't help you solve problems, you're just stamp collecting. I've seen that happen way too many times - otherwise smart people thinking they're being productive by writing a whooole lot of useless classes, where a few functions and some if statements would do it. But don't get me wrong - I'm totally in favour of having common interfaces. They aren't great because they form a taxonomy. They're great because it helps us abstract. Whether its Iterator or different software implementing HTTP, interfaces actually help us solve actual problems. > Knowing that AES and Twofish are block ciphers is useful when dealing with cryptography. The useful fact in this example is that they both do a similar thing. They form a type class from their common behaviour. Behaviour they may share with - for example - a compression algorithm. Thats what actually matters here.
- starspangled 1y agoI'm not sold on the evidence of much in the way of programming language features from the "object oriented" era. They were pushed by cultish types with little evidence. There was this assertion that all these things were wonderful and would reduce effort and therefore they must be good and we all must use them. We got object oriented everything including object oriented CPUs, object oriented relational databases, object oriented "xtUML". If you weren't object oriented you were a pile of garbage in those days. For all that, I don't know if there was ever any good evidence at all that any of it worked. It was like the entire industry all fell for snakeoil salesmen and are collectively too embarrassed about it to have much introspection or talk about it. Not that it was the last time the industry has fallen for snakeoil...
- roguecoder 1y agoIf encapsulation wasn't useful, we wouldn't write microservices. If abstraction wasn't useful, we wouldn't use containers.
- starspangled 1y agoThat's not evidence though even if we take it as true. You can of course make layers of abstraction or encapsulation without "object oriented" languages.
- strogonoff 1y agoLike any map, the inheritance pattern is bad, except when it works. It’s a strategic capability to be able to guess well which is which in given context. My first foray into serious programming was by way of Django, which made a choice of representing content structure as classes in the codebase. It underwent the usual evolution of supporting inheritance, then mixins, etc. Today I’d probably have mixed feelings about conflating software architecture with subject domain so blatantly: of course it could never represent the fundamental truth. However, I also know that 1) fundamental truth is not losslessly representable anyway (the map cannot be the territory), 2) the only software that is perfectly isolated from imperfections of real world is software that is useless, and 3) Django was easy to understand, easy to build with, and effectively fit the purpose. Any map (requirement, spec, abstraction, pattern) is both a blessing that allows software to be useful over longer time, and a curse that leads to its obsolescence. A good one is better at the former than the latter.
- Retric 1y agoInheritance was oversold, but it can help remove a lot of boilerplate code. Early windows notoriously had hundreds of lines of code for a hello world program. Setting your own defaults and getting on with your day is great for dealing with a less refined API etc. Complex inheritance trees can make sense in niche application for similar reasons.
- mexicocitinluez 1y ago> And inheritance naturally suggests grouping interfaces into a tree in the way that seems of little value because in practice a tree probably doesn't represent the fundamental truth of things. The fundamental truth of things? What are you even talking about? What fundamental truth of things? And what does that have anything to do with building software?
- jmull 1y agoSo... what even is the purpose of computer language abstractions? To provide building blocks useful for the construction of programs. There's a number of properties that are good for such building blocks... composability, flexibility, simplicity, comprehensibility, etc. Naturally, these properties can conflict, so the goal would be to provide a minimal set of interoperable building blocks providing good coverage of the desirable properties, to allow the developer can choose the appropriate one for a give circumstance and to change when needed. E.g., they could choose to use a simple but less flexible block in one situation, or a more complicated or less performant block in another. IMO, inheritance is a decent building block -- simple and easy to understand, though with somewhat limited applicability. We can imagine improvements (particularly to implementation) but I think it got a bad rep mostly due to people not understanding its uses and limitations. ...I've got to say, though, if you aren't figuring out how to use the simple and easy tools, you're really not going to do better with more complicated and capable tools. People hate to admit it, but the best of us are still highly confused monkeys haphazardly banging away at keyboards, barely able to hold a few concepts in our heads at one time. Simple is good for us.
- williamdclt 1y ago> Weird edge cases like HTTP over non-TCP protocols or rendering without screens start throwing spanners into a tree of assumptions that never needed to be made yes, but that's true of other abstractions too. Whether you use inheritance or not, you usually don't know what abstractions you need until you need them: even if you were using composability rather than inheritance, chances are that you'd have encoded assumptions that HTTP goes over TCP until you need to handle the fact that actually you need higher-level abstractions there. If you don't use inheritance, you switch to an interface (or a different interface) in your composition. If you did use inheritance, you stop doing so and start using composition. The latter is probably some more work but i don't think it's fundamentally very different.