Inheritance was invented as a performance hack
catern.com
catern.com
1. Static invariants (not subject to runtime-defined conditions).
2. Performance (AOT compilers know more about the system and can elide more code and devirtualize more calls, etc.).
I think the characterization of performance features as a "hack" is misleading. The article builds a bit of strawman, being dismissive of a performance feature for the fact it was for performance, then does a 180 in the Conclusion section saying "it's not bad" to look for performance.
Did we forget inheritance vs. composition is also for performance? Honestly I've not seen such a strong characterization of inheritance as being purely semantic. If anything, today we see inheritance as the thing a junior dev reaches for first, in order to share code, because they don't know better.
Sure, there's the "Cat extends Animal" shtick that refuses to die. But everyone refers to this kind of talk of inheritance ironically these days.
It can also produce code that is not easily and clearly extendable, if the behavior you're trying to extend is buried a layer or two further down the inheritance hierarchy.
Unfortunately, due to the First Law of Kipple, the rate at which you run into this problem is proportional to the age of the code base. And so we grow frustrated with implementation inheritance. Other extensibility mechanisms probably have similar pitfalls, but they haven't been the dominant way of doing things for long enough to accumulate the same volume of clutter.
Don Knuth has mentioned in a few interviews that he isn't so hot on code reuse, and prefers code that's designed to be easy to edit over code that's designed to be easy to extend. I'm starting to see some wisdom in that idea. With the one, being able to keep things tidy is a primary goal. With the other, eventually tidying becomes a frightening enterprise, because you have to avoid upsetting the inheritance hierarchies that are precariously balanced on top of the code you're trying to tidy up.
It's hard to define a "good practice" for this wildly heterogenous group. What's good for a beginner (training wheels on a child's bike), is completely counter-productive to a pro (motorcycle sports driver).
I can't say that I know a software development idiom that reliably creates a more stable equilibrium point. But I don't think we'll ever find one unless we're willing to examine the failure modes of existing paradigms.
Not sure I understood your example about the static class vs. function factory tbh though.
This is because Java has single-inheritance enforcement for classes.
C++ for example has multiple inheritance. So the way you do an interface is you just write an abstract class, then extend it to implement it.
Java doesn't let you have inheritance without polymorphism, but it is possible, see "private inheritance" in C++.
class ServiceWrapper:
service = ...
v1 = ...
v2 = ...
@static
def my_job():
return service.call(v1, v2)
vs.def create_service_wrapper(v1=..., v2=..., service=...):
def f():
return service.call(v1, v2);
return f
The class can scale to multiple methods sharing parameters, but the semantics of the factory fall apart if you want to return more than one parameterized function.As articulated elsewhere in the discussion, classical inheritance has a great affinity for the "specialization" design pattern, which is everywhere. Classical inheritance is not just a performance hack, it is semantically compelling, as illustrated by the enduring popularity of "Cat Extends Animal"!
Furthermore, single inheritance has a really elegant canonical implementation: "extending" structs and vtables in subclasses by appending their member variables and new virtual method pointers.
Using composition may inoculate against certain brittleness problems in code sharing, but if "has-a" is cumbersome to code up, no amount of scolding is going to change mass behavior.
Emphasizing interface inheritance as an alternative to classical inheritance seems to be more effective at breaking people away from the problems of implementation inheritance, even though interface inheritance is still "is-a" and not "has-a". And "fat pointers" with two words, one for the dispatch table and one for the object, are an elegant canonical implementation for interface inheritance.
> Sure, there's the "Cat extends Animal" shtick that refuses to die. But everyone refers to this kind of talk of inheritance ironically these days.
This sort of "everyone knows" dissing of inheritance is why I tend to dislike HN discussions of it.
For what it's worth, the rust-by-example for traits uses the Animal "schtick" unironically:
I think often it's compelling for misleading reasons. For example, is a square a rectangle? Mathematically, yes. But in mathematics, we don't mutate values (we would describe an entity's evolution as a series of values).
If you are allowed to mutate the dimensions of a rectangle object, then for a square to be a rectangle, it must set both dimensions when setting either, or otherwise cause an error if its dimensions get out of sync. If you can, say, get the area of a rectangle, Liskov's principle of behavioral subtyping suggests that such a square would break the expectations of a client of rectangles ("I changed the width but now I'm getting the wrong area!"), so a square is not really a rectangle. You may recover behavioral subtyping if you explicitly limit the kinds of reasonable inferences a client can make from a rectangle, but that may limit your use cases for actual rectangles.
I like this phrasing from one of the answers to this SO question [0]:
> The problem is that what is being described is really not a "type" but an cumulative emergent property.
> All you really have is a quadrilateral and that both "squareness" and "rectangleness" are just emergent artifacts derived from properties of the angles and sides.
Put differently, it's very tempting to treat "square" as a specialization of "rectangle", but that has very little to do with their intrinsic definitions and far more to do with what can be observed of them by the program in context.
Inheritance is the dynamic variant of composition.
You can dynamically extend objects by adding fields and methods to it's class, but not to composed objects, because they are statically closed. (Well in the good languages. javascript and friends still allow to extend objects, to violate their types).
You can also dynamically mess with the inheritance search path, or fall into the diamond trap. Not with static (or maybe call it lexical) composition.
Composition wastes a lot of space and disallows dynamic extensions, like plugin overrides, test mocking, ... But is very good with proper structural typing, and is a good performance hack, because it doesn't need to chase pointers at runtime, and neither do the inheritance search and linearization dance.
The second problem they describe is linked lists. A Java-like linked list means an extra level of indirection, where the list element holds a reference to the held object. For both performance and space reasons, they wanted to remove that layer of indirection. That meant either having their garbage collector support interior references (references to any field inside the object) or else making a reference to a linked list element be the same as a reference to the held object. They chose the second option, by way of making the element and the held object one and the same, using inheritance. A Simula linked list has as much indirection as a C++ std::list<T>, one fewer indirection than a java LinkedList<T>.
They mention reference counting, but a mark-and-sweep, copying, or tricolor tracing collector would still have this same issue if it didn't support interior references. As sophisticated as Oracle's latest JVM is, I don't believe any of its several garbage collectors support interior references. Physically in the JVM's memory, LinkedList<T> really has an extra layer of indirection as compared to C++'s std::list<T>, and the JVM uses escape/lifetime analysis in order to stack-allocate some objects that would otherwise need to be heap-allocated.
I guess you mean java.util.LinkedList that has virtually no use case that's not outperformed (both space and time) by another datastructure.
Writing your own linked list with prev(+next) pointers ain't hard by it's nothing like C++ templates. The removal of layer of indirection does work in Java [pretty well] as well - like extending AtomicReference (or AtomicInteger), if you need a CAS and some other data, of course it comes with false sharing.
Sounds broadly true, but I'm not sure it's precisely right. How could std::is_same be implemented?
You would perform manual static context-sensitive text expansion, just like the C++ template expander does. In this case, you end up just manually expanding it all the way out to a true or false by hand.
I'm just saying that the C++ template expander only performs static context-sensitive text replacements, ending in valid template-free C++ code. The template engine doesn't introduce any magic that can't be done more tediously in template-free C++ by hand, so you can do the same tricks in Java by hand.
Now, Java doesn't have non-primitive value types, so you'd have to manually flatten some classes in order to get the same layout without the class boundary forcing an indirection. Java also doesn't allow interior references while C++ absolutely does, but that's outside the C++ template expander / template sub-language.
It is true that you can do anything that templates do by hand, but that's true for any language construct (at the limit you could write asm).
The static types (and constant values in some cases) are the context. That's the "context-sensitive" part of "context-sensitive expansion".
> It is true that you can do anything that templates do by hand, but that's true for any language construct (at the limit you could write asm).
Yes. That's my point. I was replying to someone saying that they could remove a layer of indirection from a linked list by writing a custom data structure that had the next and previous references, and I was pointing out that they're essentially running C++ template expansion by hand. There's nothing surprising there.
Sure, if you move all your code into your new language that transpiles to C, but that was my point, the lack of language integration means there's plenty it can't do that C++ templates can do.
An example from [0]:
std::is_same_v<int, std::int64_t>
In C, I don't know that there's any way to determine whether int and int64_t are the same type.public class Node<T>{ Node prev, next; //stuff comes here .... }
Then extend and have the overall code that deals with modifying the datastructure, but it's overall ugly. Personally I have written enough datastructures where linking nodes is useful. For example: red/black tree + insert order 'next' makes for a decent implementation of a moving median. Yet, that's not what almost any developer would do normally.
It could be solved today, not in the sixties when Simula has been designed. It's fascinating how much scientific progress we're taking for granted, assuming that is has been that way forever.
> A Simula linked list has as much indirection as a C++ std::list<T>
Barring the same comment on the progress in generic types, intrusive lists are on a bit different scale. The difference between a T which can be a part of intrusive list and List<T> is in how T gets into a list. For intrusive list the links are already in your T. If you have an element and want to add it to the list then it's a bunch of pointer assignments. For non-intrusive list you have to allocate a new list node, and maybe even move your element there.
As a performance optimization, inheritance demonstrated value at a time when performance improvements were at least three orders of magnitude more important than they are today.
People here like to disparage OO and inheritance, but the distaste clearly is just a reaction to Java and its unfortunate lack of any other organizational mechanism, requiring abuse of the one feature for everything.
In C++, it is sometimes directly useful, with or without a vtable, independent of "OO"; and even occasionally is exactly the right thing, explicitly as an OO tool. As one tool in the toolbox, it is simple, understandable, and causes no trouble unless badly abused. It is far from the only tool, so is not often the best choice, but probably most programs of any substance have one or two places where it is better used than not.
People coming to C++ from impoverished languages like Java often fail to recognize bad habits they have internalized.
Implementation inheritance is a problem when crossing organizational boundaries, as reasonable changes upstream can impact correctness downstream.
Ultimately, there is no substitute for taste. Substituting fetishism produces unfortunate results.
[I might be really really wrong here]
One should know the technical benefits and drawbacks of programming patterns before applying them. Just because some language or environment or majority of programmers promotes it, doesn't mean that its ok to apply it. Without knowing the technical reason for applying a pattern is just cargo cult programming.
IIRC, there was impetus from Christopher Alexander's architectural "pattern language", and the desire to do something similar in software, which was a bit of an unguided mess at the time.
Oh, come on. There was https://en.wikipedia.org/wiki/Structured_program_theorem and https://en.wikipedia.org/wiki/Modular_programming. There was a discussion about functional programming, for example famous John Backus article - https://dl.acm.org/doi/pdf/10.1145/359576.359579.
“A pattern language” is from 1977; its application to programming from 1987 (http://c2.com/doc/oopsla87.html)
So, in the context of “Simula may have been one of the first to implement inheritance as a means to an end, but how could they have "invented" inheritance?”, I think this discussion is diverging a bit.
I would guess the idea of inheritance was already used in some assembly programs. It would start with people using structures that shared initial fields, and passing them to functions that didn’t care about the other fields (functions handling intrusive linked lists, for example). Passing a pointer to a function that _does_ know the exact type of the fields would be the next step.
The next step would be to allow heterogenous collections and use logic to discern between the various variants (many lisps do that by going through an “is it an atom?, is it a number? etc. chain, and likely did that years before Simula 67 appeared)
Chances are somebody also embedded a function pointer or two inside such objects, but I guess that would have come fairly late, as a function pointer in each object would have been expensive, memory-wise in the ‘60s and ‘70s.
Anyway, I think ‘inheritance’ indeed was invented/discovered, but gradually, and without explicitly naming it.
Turns out in C# there's no syntactic difference between implementing an interface and inheritance, so it makes sense in C# to explicitly state that something is an interface but, arguably, only there.
Of course, it’s an established C# convention (and inspired by the naming convention for COM interfaces) so one better sticks with it, but I think the convention was a bad choice for the reason stated above.
Problem is, sometimes the are changes in how types are composed and what used to be an interface can end up as a type alias and vice versa.
This Chicken v Egg pondering is interesting. I mean, surely the term "inheritance" was used in this instance because it already resembled something that had been established in theory?
What's interesting is, looking at this now that these two different aspects of what we nowadays consider to be a fairly well defined computer science concept both seem to have emerged around the same time.
Could it be C & Q were inspired by the Simula guys, or vice versa maybe Simula adopted the term because the implementation "resembled" the semantic definition. Or perhaps they independently emerged from concepts that were popular at the time!
After spending way to much time in Java, and OOP-style php and python, I just struggle to see the advantage of inheritance. It usually takes a long time to look through the object hierarchy to figure out where the behavior is really coming from.. and the loss of type inference outside of localized expression statements is devastating in comparison.
> Simula created inheritance instead of using composition because it allowed their garbage collector to be simpler.
In C++, inheritance (and other OO languages I assume) is implemented as a composition and when multiple inheritance of the same class happens then the members of the twice parent is inherited twice unless the virtual keyword is used. ( in other words, if B : A, A, C then sizeof B = A + A + C + B's stuff, or if B : virtual A, virtual A, C then sizeof B = A + C + B's stuff). This is why declaring on the stack Parent parent = Child() "works" by just truncating the content of the child that was appended at the end of Parent.
Personally I like to use private inheritance as a composition shortcut since for all intent and purpose it's achieves the same goal with less boilerplate. Outside of that I do use it if it strictly follows the substitution principle with other the members of the base class being used just as much as the child's. Which happens very rarely but does happen. It's like an Allen's key, you probably won't need it except when you must disassembled your IKEA furniture when moving out.
I think what TeeMassive is saying is that this rejigging is a simple truncation because the memory layout of (non virtual) inheritance in C++ is a concatenation of Parent and Child objects. I'm not sure whether this is implementation specific though.
The part about “rejigging” and simple truncation happens before the constructor call when you cast ref-to-Child to ref-to-Parent.
Parent p = Child(); calls it by implicitly casting Child& to const Parent&. That’s what happens.
I see that you must have meant this casting step, so indeed the constructor has no control over the rejigging.
Say you have the obvious minimal implementations of
struct Point ...
struct Point3D : public Point ...
And then you do this: Point p3 = Point3D(1.1, 2.2, 3.3);
Point p = p3;
On a 64 bit machine this results in sizeof(p) == 16 and sizeof(p3) == 24. So, at some point someone has to decide how to transform p3's 24 byte representation into p's 16 byte representation.In C++, this happens simply by truncting p3, i.e copying the leading 16 bytes of p3 into p and throwing away the trailing 8 bytes (I'm not sure whether or not this is part of the standard).
This is not the only imaginable way in which this could possibly happen though, which is why I'm saying that the whole process is not sufficiently defined by stating that a reference to child is cast to a reference to parent.
https://wiki.c2.com/?KnowledgeRepresentations
https://wiki.c2.com/?TypeInferenceStory
https://www.cin.ufpe.br/~mtcfa/files/in1122/Knowledge%20Repr...
I had thought of building the history system on top of a theory of patches system would be a nice stretch goal. But it seems to me that having an infinite undo function drastically changes the account creation situation. I only have to be passingly sure you’re not a bot, and I can decide after the fact if you’re approximating a decent human being or not.
SICP doesn't. :) Which is why it's such a great book, of course.
I believe it was Collins and Quinlan did the seminal work on the conceptual side, but what's interesting is it seems to emerge around the same time ... considering how inextricably bound the two are in computer science it's an interesting chicken/egg problem!
Fyi... The "knowledge representation" aspect of OOP inheritance is emphasized by computer science professor Andrew P. Black. He wrote a long paper[1] about it and also has a video[2]. It's unfortunate that Black's alternative perspective (which he shared with Philip Wadler) is not discussed as often as "Object-Oriented Programming is Bad"[3] videos.
His paper is a long read (over an hour) that covers the intellectual history of OOP starting with Simula/Smalltalk. The following is an excerpt from https://www.sciencedirect.com/science/article/pii/S089054011...:
Most of us can grasp new ideas most easily if we are first introduced to one or two concrete instances, and are then shown the generalisation. To put this another way: people learn best from examples. So we might first solve a problem for , and then make the changes necessary for 4 to approach infinity. [...]
To illustrate the power of inheritance to make complex abstractions easier to understand, letʼs look at a case study from the functional programming literature. In Programming Erlang [19, Chapter 16], Armstrong introduces the OTP (Open Telecom Platform) generic server. [...]
To make sure that the message about the way that the OTP server works does sink in, Armstrong presents us with four little servers … each slightly different from the last. server1 runs some supplied code in a server, where it responds to remote requests; server2 makes each remote request an atomic transaction; server3 adds hot code swapping, and server4 provides both transactions and hot code swapping.
Each of these “four little servers” is self-contained: server4, for example, makes no reference to any of the preceding three servers.
Why does Armstrong describe the OTP sever in this way, rather than just presenting server4, which is his destination? Because he views server4 as too complicated for the reader to understand in one go. Something as complex as server4 needs to be introduced step-by-step. However, his language, lacking inheritance (and higher-order functions) does not provide a way of capturing this stepwise development.
In an effort to understand the OTP server, I coded it up in Smalltalk. [...] First I translated server1 into Smalltalk; I called it BasicServer, and it had three methods and 21 lines of code. Then I needed to test my code, so I wrote a name server plug-in for BasicServer, set up unit tests, and made them pass. In the process, as Forsythe had predicted, I gained a much clearer understanding of how Armstrongʼs server1 worked. Thus equipped, I was able to implement TransactionServer by subclassing BasicServer, and HotSwapServer by subclassing TransactionServer, bringing me to something that was equivalent to Armstrongʼs server4 in two steps, each of which added just one new concern.
Once I was done, I discussed what I had learned with Phil Wadler. Wadler has been thinking deeply, and writing, about functional programming since the 1980s; amongst other influential articles he has authored “The Essence of Functional Programming” [21] and “Comprehending Monads” [22]. His first reaction was that the Erlang version was simpler because it could be described in straight-line code with no need for inheritance. I pointed out that I could refactor the Smalltalk version to remove the inheritance, simply by copying down all of the methods from the superclasses into HotSwapServer, but that doing so would be a bad idea. Why? Because the series of three classes, each building on its superclass, explained how HotSwapServer worked in much the same way that Armstrong explained it in Chapter 16 of Programming Erlang. This was an “ah-ha moment” for Phil.
To summarise: most people understand complex ideas incrementally, by starting with a simple concrete example, and then taking a series of generalisation steps. A program that uses inheritance can explain complex behaviour incrementally, by starting with a simple class or object, and then generalising it in a series of inheritance steps. The power of inheritance is that it enables us to organise our programs incrementally, that is, in a fashion that corresponds to the way that most people think.
[1] https://www.sciencedirect.com/science/article/pii/S089054011...
Definitely! See [0]. Personally, I came across Frame-based knowledge representations in the mid-1980. For reasons that were frankly insane, I was trying to code a frame-based knowledge representation in distinctly non-OO K&R C as part of a collaborative university research project. I moved into industry in 1988 and used OO for the first time (the Common Lisp Object System) and finally started with C++ in around 1993. Obviously this was my personal career trajectory but I encountered knowledge representation research before mainstream OO languages.
[0] https://en.wikipedia.org/wiki/Frame_(artificial_intelligence...
var o1 = { a: 1, b: 2, c: 3 }
var o2 = { x: 1, y: 2, z: 3 }
o1 = Object.assign(o1, o2)
o1.z // 3
o1 has now inherited o2. I don't see much difference between this and classic untyped OOP.Edit: copying functions over, not values, is what I’m getting at. Values used for simplicity of example
function f() {
function g() {
return 1
}
return { g }
}
function f1(f) {
function g() {
return f.g() + 1
}
return { g }
}
f1(f())Edit: Not to mention that storing function pointers with each object is a huge waste of memory, versus storing one class pointer in each object, and having that class object store function pointers and parent class pointers. That way, you guarantee that each function pointer is stored exactly once, and each object has a single sizeof(pointer) of overhead (and even this can be improved with more complex implementations).
Implementing OOP in JS with hacks like this is worse, in pretty much all ways, apart from the intellectual kick many devs get of avoiding OOP at all cost, as opposed to just using built-in language features.
But copying function pointers seems negligible. I can’t see this being avoided even in traditional oops.
Although today we have a much better grasp on formal semantics, which makes it easier to make up new ideas. But those new ideas aren't likely to ever displace the old ideas popularly, and it's hard to come up with them in the first place since old ideas live in your head...
First, inheritance from an interface/trait is totally okay. The problem is class inheritance, meaning implementation inheritance.
There are two cases:
1) you inherit from a class and only add methods but don't overwrite anything. This is the good case, you can do that
2) you inherit from a class and overwrite one ore more of its methods. This is the bad case, no matter how you look at it. And here is why.
Look at the following classical simple Stack class:
class Stack(...) {
def push(element) returns nothing = ...
def pushAll(elementlist) returns nothing = ...
def pop() returns element = ...
}
Now imagine you want to keep a count of the number of elements, so you decide to extend the class: class CountingStack(...) extends Stack {
override def push(element) returns nothing =
count += 1; super.push(element)
override def pushAll(elementlist) returns nothing =
count += elementlist.size; super.pushAll(elementlist)
override def pop() returns element =
count -= 1; super.pop(element)
}
Looks good no? Can you spot the problem?If the Stack class implements pushAll as iterating over the elementlist and calling push for each element, then the CountingStack will count twice!
We could try to just not overwrite pushAll in CountingStack - but what if the Stack class is later changed to implement pushAll without calling push? Then CountingStack would now miss counts.
This problem is impossible to fix and it is a general problem - when overwriting a method, you can never be sure that semantics could break when the base class is changed.
So what is the solution here? Simple: use composition over inheritance.
class CountingStack(underlyingStack) {
def push(element) returns nothing =
count += 1; underlyingStack.push(element)
override def pushAll(elementlist) returns nothing =
count += elementlist.size; underlyingStack.pushAll(elementlist)
override def pop() returns element =
count -= 1; underlyingStack.pop(element)
}
Problem solved - no matter how Stack is implemented, our CountableStack will always be correct. The drawback (if you want to call it one) is that we need some common interface/trait that both Stack and CountableStack will implement.I hope that sheds some additional light on why (implementation) inheritance is often considered a bad practice.
Optimally, if your language supports it, just define an interface with these 5 methods and then define extension methods that work on any type that implements the interface. The reason why this works is that all the other functions are usually helper functions / convenience functions and they only need the other 5 functions to work, so no need to access any inner/private properties.
That is by far the most lightweight solution, and it works even for 3rd-party libraries where you can't control the types.
If your language does not have extension methods, then you can still use composition just like in the example I gave and delegate to the base class. That is a bit more code to write (because you have to delegate to the non-5 methods if you language doesn't automate that for you, some do) but I find it cleaner than hoping that no one overwrites any non-5 method.
But yeah, essentially what you are saying here is to ask people to follow the rule I gave by convention - depending on that context that might work as well.
Your example with the counting stack made use of "super", which is always a red flag to me. "super" is such a bad smell to me, that I literally never use it. In fact, in Virgil, my language project (http://github.com/titzer/virgil), there is no super construct at all, nor static methods or interfaces for that matter. 15 years of writing it, 200k lines later, and I can say that personally, having delegates, first-class functions, partial application, and tuples go way further than more complex trait/interface/extension method madness.
> Your example with the counting stack made use of "super", which is always a red flag to me. "super" is such a bad smell to me, that I literally never use it.
I think that's a sign that you have developed a good intuition of that it can lead to problems! :)
I'm not sure what's so special about overwriting methods here. There are many cases where semantics can break as a dependency changes. One example would be whether a callback is executed on the same thread (or event-loop tick) or in the background. A function might assume that the callback should be pure and thus in a later version switch where it's being executed. If your code depended on it being executed in a certain way, then it'll now be broken. The problem here is that there were certain constraints of the function which you weren't aware of (possibly because it wasn't documented).
A base class will have a similar constraints that you, as a subclasser, must be aware of.
Your stack example is also a good example of where inheritance is actually useful:
class CountingStack(...) extends Stack {
override def push(element) returns nothing =
count += 1; super.push(element)
override def pop() returns element =
count -= 1; super.pop(element)
}
Assuming that the Stack class specifies that all modifications will go through the push and pop methods, this ensures that the count is maintained regardless of how many other utility methods that exists on Stack (i.e. clear()). You don't have to extend the CountingStack with extra methods as the Stack methods gain more methods.If we cannot assume synchronous execution, then semantics can break exactly as you said. That a rather orthogonal problem from the inheritance one and my suggested solution is to make use of pure functional programming (hence purity can be assumed unless the method signature indicates that an effect might happen). Here is an example of how a potentially async Stack might look like:
class Stack(...) extends Stack {
def push(element) returns IO[nothing] = ...
def pop() returns IO[element] = ...
}
The IO would describe an action that can be ran at a later point, which means when the method has being called and returned, nothing has happened until you call `run(io)`.That way you will be immediately aware of the behavior and adapt your Countstack as follows:
class CountingStack(underlyingStack) extends Stack {
override def push(element) returns IO[nothing] =
underlyingStack.push(element).andThen(nothing => count += 1; return nothing)
override def pop() returns element =
underlyingStack.pop(element).andThen(element => count += 1; return element)
}
That ensures that order of events is being kept and also that failures (e.g. a concurrent thread dies in the middle) are handled.> Assuming that the Stack class specifies that all modifications will go through the push and pop methods
This is a severe restriction on how the Stack can be implemented then and it bears the risk of someone violating this specification by accident (think about the famous equals/hashCode specification in Java).
I think in recent years there's been an uptick in clever sounding attacks on common PL features like inheritance and exceptions, many of which look suspiciously like motivated reasoning. At the very least the arguments are extremely weak. This article ends by saying:
"Personally, for code reuse and extensibility, I prefer composition and modules."
But these are orthogonal. Languages like Kotlin have built-in support for inheritance, modules and composition. It's not an either/or approach, and there are lots of high quality, highly successful codebases that use inheritance extensively which would be pretty unimaginable without it. I use libraries that use inheritance every day and it's very rarely a problem: only in cases where someone made a bad API with it, and you can get bad APIs that rely on composition or badly modularised APIs too. I don't feel like one problem is more common than another.
Rust's Send and Sync traits solve this particular problem by putting the thread-send safety in the type signature. If the library changes to use a background thread and you have passed it a non-threadsafe closure then you'll get a compile error.
The problem with implementation inheritance specifically is that there is no single responsibility for establishing this semantics. Every method call on overridable methods incurs a dispatch step involving the whole of an arbitrarily extensible inheritance hierarchy, and every method can be called by an unknown extent of user code. You just can't give a proper semantics to it without looking at the whole-program level, which is antithetical to modularity.
Virtual should be used sparingly.
People write working code using inheritance, it's not impossible, just requires extra care. One example:
class CountingStack() {
def push(element) returns nothing =
count += 1; super.push(element)
override def pop() returns element =
count -= 1; super.pop(element)
}
Another way is to use non-virtual methods in the base class where you don't want people to modify your code. In C++ it's even the default. So you should consciously think when you make a method virtual. class Stack() {
def nonvirtual private push_internal(element) returns nothing = ...
def push(element) returns nothing = push_internal(element)
def pushAll(elementlist) returns nothing = ... // use push_internal if needed
def pop() returns element = ...
}
Anyway - I agree inheritance is bad, and I dislike OOP in general, just wanted to nitpick.IMHO the main problem is encapsulation. People expect the code to work one way and don't check their assumptions because of the whole tower of abstractions and hiding what actually happens behind layers. And then instead of writing straightforward code to do what has to be done - you have to tiptoe around all possible implementations :)
I agree with the non-virtual methods though - they are pretty what I would call conceptual composition.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
trait Stack {
fn push(&mut self, element: i32);
fn push_all(&mut self, all: Vec<i32>) {
for element in all {
self.push(element);
}
}
fn pop(&mut self) -> Option<i32>;
}
As far as I know it's impossible to invoke `super` to get at the default version of `push_all` from an `impl` which overrides `push_all`.This is still "implementation inheritance", because the implementing type inherits the default implementation of "push_all". But it seems less brittle than classical OOP implementation inheritance.
• No "super" invocations.
• Shallow inheritance hierarchies.
• No direct member variable access from the trait (interface) code.
Hence it is okay.
However, if you where to have another private function "private_push_all" and "push_all" could decide whether to call "push" or "private_push_all" then we would have the same situation again.
And it's true that inheritance IS a performance hack--not for the children, but for the parents. Inheritance incentivizes parents to accumulate wealth not just for one lifetime but for multiple lifetimes. (Granted it may paradoxically have the opposite effect on the children as you mention.)
Here's Milton Friedman on the matter: https://www.youtube.com/watch?v=km9OCw3f5w4
https://unabombermanifesto.com/#THE%20POWER%20PROCESS
It depends on how much you provide for your offspring according to this deranged terrorist. And I've read stories from billionaire kids not living "the good life" because they never have to fight/work for anything.
I think rich people tend to be people obsessed with work, giving access to money is easier than teaching their kids to earn stuff - it is a different problem.
If all my money were stolen when I die (let's say, 100% inheritance tax), I wouldn't have much incentive to grow my business more once my business makes more money than I can possibly spend in a lifetime.
Sure, some people may still do it because they love working or because they love providing for their society.
The main reason I want to accumulate money, is so I can offer them a job instead of them going through the corporate bullcrap I had to do when needing some starting capital.
Bigger businesses are capable of bigger bets (think Elon Musk) which benefit everyone, removing an economic incentive doesn't sound wise for innovation and growth.
Biological inheritance, property inheritance, however, is not.
> If all my money were stolen when I die (let's say, 100% inheritance tax), I wouldn't have much incentive to grow my business more once my business makes more money than I can possibly spend in a lifetime.
Even if one agrees that that is true, that’s not an argument that inheritance is natural, that’s an argument that it is a feature of society chosen to incentivise people to accumulate wealth surplus to their personal needs.
https://ideas.ted.com/humans-arent-the-only-ones-that-help-o...
>going through the corporate bullcrap I had to do when needing some starting capital.
The corporate bullcrap you had to go through is a direct consequence of someone growing their business beyond what they could spend in a lifetime.
If John owns an item, he can say what happens it. So if he says to an notarised person 'when I die X becomes property of my son James' then that's simply respecting his wishes over his property.
Is it much different if John choose to give something away moments from death, or if he asks someone else to arrange the giving on his behalf after death?
We respect the wishes of the dead because it's part of the continuance of life and civilisation (which is itself fundamentally the store and passing down of value, I.e. inheritance). The same reason we'd honour someone who died for freedom, or honour the funeral wishes of a family member. Their wishes are worth much to everyone who knew them. The same should apply to personal property.
I think I'm stabbing in the right direction. A better answer might come from a better read or more philosophical mind than mine.
Then you're selfish and no good for the money anyway.
One can only buy so many jets and sports teams, sure.
But, if society endows you with that sort of reward, it becomes your responsibility to move society forward with that blessing.
Anything less is exactly why we live in Trump world and not some sci-fi future we all imagined as kids.
Honestly not sure I'd even bother getting out of bed most days if the future of my children was just in the hands of the state, what would be the point.
Life itself, even for those of us without children, is an incredible experience.
This is why I'm not a biologist (or OOP fan). :)
By what metric? Or do you mean "a way to avoid having to be a high performer in order to achieve good results"? Isn't the whole beef with wealth inequality that given otherwise equal luck and faculties, a person who starts with more capital will gain more capital?
That's the point: a leasurely trust-fund baby will likely be more "successful" than a hardworking and "driven" poor person.
For example, if a "manipulative anti-social" gene was increased in prevalence because it maximized net worth.
The problem here is a common issue in game theory, optimization without regard for process. Your inheritance doesn't care how it was acquired.
Sure this could be offset for positive genes, but that's making one big assumption.
The assumption that capitalism is the only reward function for our species. That is, what other forms of inheritance reproductively compete with money?
With a good type system that includes traits, you almost never need virtual dispatching, as any Rust developer may tell.
Take the following example I have found on the net: https://play.rust-lang.org/?version=stable&mode=debug&editio...
In that example, both the bicycle and the car have the property `speed`. Imagine you have multiple types now that need to have the `speed` property. You would need to copy-paste the same code for each new type in order for you to be type safe.
Apparently it is called *Monomorphization*: https://cglab.ca/~abeinges/blah/rust-reuse-and-recycle/#mono...
From the article:
* But if I want a single queue to be able to handle different tasks, then it's not clear how that could be done with monomorphization alone. That's why it's called "mono"morphization. It's all about taking abstract implementations and creating instances that do one thing. *
Which was exactly what I was experimenting with: A single queue worker that can handle different cases. Honestly, it made Rust almost not worth it for me. Sadly, I was too deep to turn back so I wound up doing the whole thing in Rust. I have tons of copy-paste code. It is ugly and it is bothering me.
The need for something to solve the problems inheritance solves has been known in Rust for a long time. Mostly it's been motivated by the need to implement the HTML DOM, which is fundamentally an inheritance hierarchy, in Servo. There's a longstanding RFC about it:
https://github.com/rust-lang/rfcs/issues/349
Inheritance is one possible solution. There are others.
Another issue I have with it is perfectly described in this article: https://theta.eu.org/2021/03/08/async-rust-2.html
For what it's worth traits largely prevented copy and paste and where traits fail there are macros. The classic inheritance example you link to is a tiny percentage of my code and an orders of magnitude smaller time sink when compared to the code maintenance problems I faced in other languages.
True, but not that interesting? Sure we could have some Ruby :get_attrs magic or whatever.
fn id<T>(t: T) { }
fn main() {
id(String::from("foo"));
id(1_usize);
}
The Rust compiler will generate a method of `id` that works for both String and usize, so there will be two copies of the function with a slight variation in your binary. That process is called monomorphization.> Which was exactly what I was experimenting with: A single queue worker that can handle different cases. Honestly, it made Rust almost not worth it for me. Sadly, I was too deep to turn back so I wound up doing the whole thing in Rust. I have tons of copy-paste code. It is ugly and it is bothering me.
You can easily use generics, there is no reason to copy paste code like this
You can also do this:
struct Vehicle
{
speed: f32,
type: VehicleType
}
enum VehicleType
{
Car(...),
Bicycle(...)
}
Or this (although this is the least common): struct Vehicle
{
data: VehicleData
type: Box<dyn SpecificVehicle>
}
struct VehicleData
{
speed: f32,
}
trait SpecificVehicle
{
fn quack(&self, data: &VehicleData);
}
impl SpecificVehicle for Car {...}
impl SpecificVehicle for Bicycle {...}As you can see I also used a macro by example there to remove some of the duplicated code that you would otherwise have.
> You cannot compose types in Rust. You compose behaviors not types.
But I thought in OOP behaviour is type? (Or, IOW, type includes behaviour.) To me, judging only from this, it feels like Rust isn't quite OO... Is it perhaps just not quite finished yet?
Another aspect: That whole "Rust has something called 'monomorphization', which leads to lots of copy-pasting" reinforces that impression. Is this the same problem that C++ tries to overcome by the "select which inherited implementation to use" operator, and other languages by allowing inly single inheritance?
"Well, mmm, duh?", thought programmers of other languages.
"I've designed this project in Rust, following Rust-imposed design principles, and found monomorphism sufficient", said the Rust programmer.
"Well, mmm, duh?", thought programmers of other languages.
"I've designed this project in Python, using Python-style design and I've found that I did not need strictly typed functions", said the Python programmer.
"Well, mmm, duh?", thought programmers of other languages.
"I've designed this project in language X, which impose Y and as such I've found that you actually don't need Z", said the X programmer.
"Well, we got the picture already", said the HN reader.
On a more serious note, thinly veiled threats by patriarchs to their children working in the family business regarding their eventual share in the business is something that probably happens routinely around the world, especially in areas like Europe, East Asia, South Asia, and South East Asia, with many multi-generational privately held family businesses.
The HN appetite for posts ragging on inheritance will never be sated.
I'm happy these days using languages like Rust and Go that avoid classical inheritance, so I'm not here to argue in favor of inheritance. I'm just irritated by the pile-on.
Also, the way CLOS does inheritance avoids a lot of the drawbacks too by decoupling classes and inheritance from behavior.
As for what benefit - your example with Ruby can be reversed and I can ask you the same question.
Anyways large part of a programming is an art and different people have different approaches. If you do not like the concept do not use it. Just do not assume that what you think is the "right way".
To be clear, I do agree with your assertion, however I'm having a hard time actually putting "how/why" into words.
A list containing two different subclasses of your avoiding-copy-paste base class is now most likely a logical error.
1. Composition is often more explicit than inheritance. Inheritance is overly magic.
2. Composition avoids incidental coupling of methods. With inheritance this is unavoidable.
3. Composition is more difficult to misuse. Both composition and inheritance have their purposes. In the wild, I’ve seen inheritance misapplied much more often than I have seen with composition.
So don't allow those? Supporting inheritance doesn't mean you have to support heterogeneous lists.
For example, if you call the same function from many locations those locations are now coupled since if you change the function you change all of those locations behaviour. Many times that is desirable, in which case it is good coupling. The exact same rule applies to inheritance.
The only problem with inheritance is that people can use it for classes that weren't written to be base classes, and therefore a ton of bad programmers use it for code reuse without thinking about the contract it is supposed to implement at all. With that in mind, allowing inheritance only for abstract classes isn't an issue at all.
All code reuse can be expressed with composition and delegation, bringing more flexibility, testability, cohesion, decoupling, etc, etc, etc.
Having the interface implement functions does exactly that, so your point only applies for interfaces without implementations.
That or abstract class/pure interface. There's no need for default implementations introducing assumptions in code.
While I take your point about introducing assumptions, I'm reluctant to give up the convenience of default implementations; they seem to present fewer problems than classically inherited methods because shallow hierarchies are more common with interfaces than with classical inheritance, and because default interface methods cannot directly access member variables because they do not know about object layout — unlike methods inherited from a parent class under classical inheritance.
https://docs.oracle.com/javase/tutorial/java/IandI/defaultme...
The latter is a more general classification?
A type which inherits a default method implementation from an interface is an example of "inheritance for code reuse".
A type which inherits a method implementation from a parent class under classical OOP is also an example of "inheritance for code reuse".
However, I argue that "interface inheritance with default implementations" is superior to "classical OOP" because it avoids tight coupling with memory layout, problems with implementing multiple inheritance in classical OOP, etc.
it's literally the same thing in practice
> including the diamond problem,
the diamond problem has only ever been a "problem" in OOP textbooks, in years and years of working on OO system I have never saw it be a problem in practice
https://www.usenix.org/legacy/publications/compsystems/1989/...
The plan seems so complex that it makes complete sense to me why languages would avoid multiple class inheritance (where each class is afforded direct access to member variables).
In contrast:
• With single inheritance, you don't have to resolve different member variable layouts because there is only one.
• With interface inheritance (with or without default methods), interface methods don't get to access struct members directly and know nothing about object memory layout.
"The diamond problem" only occurs with multiple inheritance. At a (wild-ass) guess, only a minority of "traditional" (=inheritance-based) OOP languages have that; certainly not all of them. So as far as "the diamond problem" is concerned, interfaces with default implementations are no better than single-inheritance classes.
Using composition or lay out data differently may yield designs with more desirable properties. There's no one answer and it depends.
Does that depend on how inheritance works in a particular language? For example, in Python it’s widely believed that inheritance is simply “a tool for code reuse” [0].
Specialization remains a very common design pattern that is incredibly useful and trivially and intuitively solved with inheritance. No other programming concept (HKT, ad hoc polymorphism, functional programming, etc...) comes close to its elegance.
Beautifully put. That matches up with my own experience learning classical OOP, even if I'm now comfortable with other models.
When someone says that "inheritance is bad for code reuse" they're not talking about interfaces, or using inheritance for polymorphism. They're strictly talking about sharing code using implementation inheritance, which is the thing that has been widely criticised for more than 30 years now.
One can argue that even the "Template Method Pattern" doesn't fall into "implementation inheritance", since the implementation lives in the subclass.
If you read the posts, discussion is way more nuanced than "inheritance bad vs inheritance good".
Here is more specifically what I meant with my comment about specialization above. There is a class with four methods, three of which are exactly what you need but the fourth one, you need to modify.
Solving this with inheritance is trivial (extend and override).
Solving this with any other paradigm is... much harder and requires a lot more boilerplate.
But anyway, creating a new method without changing any of the methods of the super class I think it's generally ok. The problems arise from modifying methods that the super class already implemented.
You may be missing the point that's being made, though. No one is arguing against interfaces, but overriding concrete methods from a concrete class. Those need to be well thought out as extension points for you to have any chance of having stable software. Not quite for free.
Somewhere deep in the code is calling a.foo(), but when you pass a subclass of A that overrides foo(), then this code "magically" calls that new implementation.
This is where specialization shines and no other paradigm allows this so elegantly and so simply.
It's a bit of interesting history. I don't know why it would get a "Fine. But unrelated stuff" response.
> Personally, for code reuse and extensibility, I prefer composition and modules.
For others who may be interested, poked around and found that it is a quote from The Street of Crocodiles and Other Stories, by Bruno Schulz.
https://www.goodreads.com/book/show/1576188.The_Street_of_Cr...
I'm a bit deflated though, that now that this subthread has been demoted and dropped to the bottom, the usual "everybody knows inheritance sucks" comments are accumulating up at the top. I really enjoyed having a discussion for once that tried to suss out why inheritance, despite its flaws, was historically so semantically compelling.