In my experience its always really telling that there are no incredibly simple is-a relationships to use in inheritance justifications (as opposed to tons of goto real world examples for just about every other feature under the sun). Its always either incredibly abstract (B is-a A), or incredibly contrived (Triangle is-a Shape). I've spent a lot of time in inheritance heavy code, and I've yet to find something that wouldn't be just as, if not more, elegant without inheritance. I've spent the most time in Cocoa (which I believe to be very well designed BTW), but the inheritance there is clearly not needed IMO. I usually find one of the following to be true:

1) Very shallow inheritance trees that could have very easily (and more logically) been replaced with interfaces. For example, NSResponder being everything's superclass even though just about all of its methods are empty implementations ("subclassers responsibility"), aka, it clearly should have been an interface.

2) Confused/strangely complex is-a relationships (mutable array is-a immutable array, what? so if I specifically specify NSArray, I may still get a mutable array and the type system will be happy???).

3) Strange rules around what methods are overridable, and more importantly, how specifically they can be overridden. Why can't I in a UIView subclass override -subviews to ensure that it always has the same subviews? Well, implementation detail, that's why. Normally this would be fine, but since anyone is allowed to muck around in a subclass, suddenly I need to know the way it was implemented. This of course conflicts other parts of the framework where you are definitely expected to override the method and not use a setter.

I have yet to be presented with one of these "killer" is-a relationships that must exist. If the sole excuse is "performance", then sure I'll conceded. I guess I don't work in environments where member access is the performance bottleneck so I guess I can't relate.

Views in UI toolkits are the best example I know for an is-a relationship. A button is a view, a slider is a view. A container may have heterogenous children, but all of them are views. An interface doesn't work, because you would have to implement basic properties like containment, geometry, etc. separately for every widget.

To my knowledge, nobody has found any better design than this, though some have found worse ones (e.g. HTML and its bizarre input element).

To your specific points:

1. Yes, NSResponder should definitely have been an interface. See for example the weird way that documents and delegates are spliced into the responder chain.

2. The fact that NSMutable* is-a NS* is a lovely design. Mutability can be understood as adding setters to a class that doesn’t have them. The alternative seems to be weird duplicative splits like ArrayList/Array, or String/StringBuilder/CharSequence.

3. Agreed; it’s a failing of ObjC that it’s usually unspecified whether a method is designed to be called, to be overridden, or both. I wish this were enforced at the language level.

> In my experience its always really telling that there are no incredibly simple is-a relationships to use in inheritance justifications (as opposed to tons of goto real world examples for just about every other feature under the sun). Its always either incredibly abstract (B is-a A), or incredibly contrived (Triangle is-a Shape). I've spent a lot of time in inheritance heavy code, and I've yet to find something that wouldn't be just as, if not more, elegant without inheritance.

The flow tree (render object tree) in Servo (or any other browser engine) must use inheritance: we have a heterogeneous tree of objects that all share a common set of fields (position, intrinsic widths, collapsible margins, some various bits that store state during reflow), but they all use virtual methods because they must lay out their contents differently.

We can't use composition because we wouldn't get virtual methods. We can't use an interface because then we would be forced into virtual dispatch for all of those fields that are shared between flows.

Rust doesn't have OO yet either, so we're forced to hack around it in weird ways (usually via a small amount of unsafe code to simulate inheritance).

> I have yet to be presented with one of these "killer" is-a relationships that must exist. If the sole excuse is "performance", then sure I'll conceded. I guess I don't work in environments where member access is the performance bottleneck so I guess I can't relate.

A browser engine is exactly that sort of environment. Forcing all member access to go through virtual dispatch would murder the performance of any browser.

Note that this was exactly the sort of thing that OO was designed for in Simula: heterogeneous trees of objects that all share some common fields but have different virtual methods. This generalizes to GUI libraries, game worlds etc—in short, simulations :)

> The flow tree (render object tree) in Servo (or any other browser engine) must use inheritance: we have a heterogeneous tree of objects that all share a common set of fields (position, intrinsic widths, collapsible margins, some various bits that store state during reflow), but they all use virtual methods because they must lay out their contents differently.

Haven't used Servo, but one of the big eye opening composition experiences for me was Unity's Scene Graph. Whereas Cocoa uses an inheritance model for its view-tree, Unity has a tree of transforms that you do not subclass or change in any way, and then you add behaviors to those transforms. If you want it to render, you can attach a renderer, if you want to hit test, you attach a collider. If you want any arbitrary other thing to happen, you create that behavior. Its really nice, the idea of "tree" is completely separate from all other concepts. Rendering a 3D game, on mobile, at 60fps (on GC-ed Mono no less), makes me feel pretty good about its performance characteristics. Most our perf issues were with limiting draw calls and optimizing shaders, not method calling.

Similarly, I worked a lot on a browser engine in the past and virtual method dispatch was again not this clear cut performance killer.

> Most our perf issues were with limiting draw calls and optimizing shaders, not method calling.

Sounds like the work done by tree traversals weren't high overhead in general for your workload. But it does matter for some workloads.

> Similarly, I worked a lot on a browser engine in the past and virtual method dispatch was again not this clear cut performance killer.

We're seeing large gains from, as far as we can tell, having fewer virtual method calls than other engines. Eliminating virtual dispatch opens up a huge range of call-site optimizations since the methods can often be statically inlined (as well as reducing the load on the branch target buffer).

> Similarly, I worked a lot on a browser engine in the past and virtual method dispatch was again not this clear cut performance killer.

That doesn't match my experience. Devirtualization opens up lots of inlining opportunities, and inlining is one of the most critical optimizations that compilers can do (mostly because of the other optimizations that it opens up; e.g. const propagation, GVN, etc. etc.)

We can't use composition because we wouldn't get virtual methods.
We can't use an interface because then we would be forced into virtual dispatch for all of those fields that are shared between flows.
we have a heterogeneous tree of objects that all share a common set of fields (position, intrinsic widths, collapsible margins, some various bits that store state during reflow),

You're description seems to me to scream functions: That work on plain structs.

I'm not an expert in performance though so I don't if that is just as bad as the other alternatives you mentioned. But you didn't mention them so I thought I'd ask if you considered that as a valid approach and if so why you rejected them?