It's not like they're without the footguns - they have plenty. But even with those footguns typeclasses (as I'd rather call them) are complexity escape hatches.
E.g. at some point your type can have dozen of different implementations and you can decide to ignore that fact completely while proceeding to work on your (very narrow) piece of pie ignoring vastness of universe[^0] you found yourself in.
That being said multiple inheritance fits the same category and with minimal squinting same features can be found in languages such as Clojure or Elixir.
As other commenters mentione this is not a secret by any means, as composition over inheritance advice is decades old, but IMO still worth repeating because there might be someone who doesn't know it yet :)
[^0]: By universe I mean code implementional universe, not The Universe
class Duck(Swimmable, Quackable, FishEatable...)
I think there's still a place for "inheritance" based approach for APIs that need to be very strict about subtyping: would be hard to express covariance/invariance/contravariance without it.It's a bit like saying JavaScript's prototypes are classes even though they're technically not (even with the introduction of the `class` syntactic sugar), but for casual discussions it's probably fine to just say "JS class".
But to your point: I wouldn't really phrase the way the GP did; it makes it seem like they're on the same level of usefulness as a type class!
It was even already mentioned in the famous Design Patterns book from the GOF in 1994.
Problems with inheritance, such as the fragile base case problem, are intrinsic to inheritance, and not any specific language's implementation of it.
People favour modules to compose (e.g Enumerable), and more generally interfaces (e.g duck-typed StringIO vs IO/File/etc...)
If you throw in RBS and start to type Ruby things then you start to see that Go-style interfaces pop up quite frequently.
The key difference compared to OOP inheritance is simply that generic typestate is explicitly anti-modular: your "base" and "derived" slices are expressly contained within a single program module. You still have extensibility - you can create new "derived" slices to go with some existing "base" - but now there is no attempt to use implementation inheritance as a "programming in the large" strategy or pattern; that is done by using more robust modularity mechanisms.
What's "deceptive" about anything here?
The name "new" has no special meaning in Rust. It's conventionally used to denote a method that creates a new instance of a struct, but there's absolutely nothing in the language itself that forces you to use that (or a similar) name.
I don't think object-oriented languages have exclusive rights to the word "instance", either? It's a pretty natural word to use in that context, and it's been used like that for a long time. For example, from K&R C second edition:
> If the declaration is tagged, however, the tag can be used later in definitions of instances of the structure.
Or
> Therefore, it is impossible to declare a structure or union containing an instance of itself.
Just like "goto" in the days of old, it is best used sparingly.
It's a bit of a tangent, but I think the "class Dog extends Animal" type of tutorial did a lot of damage to impressionable programmers. Because it's completely abstract and basically meaningless, it's impossible to look at it critically and discuss why you would choose this or another approach, so the idea of class hierarchies just becomes a sort of dogma (so to speak).
The introduction to class inheritance started with the classic "Dog extends Animal" example. Honestly though, that was actually really good for new programmers in the course for exactly the reasons you point out: it's completely abstract and meaningless without immediately inviting urges of "well, why wouldn't I just do something like <x> instead" yet is also relatively straightforward way to learn "well, what the hell even IS class composition?" before you move on to the "more practical" examples where it can start to be better to dive into "well, when and why would I USE inheritance over something else?" type probing.
The course actually did pretty good with both halves of that around the Dog/Animal example... and then it all fell off the rails a bit as inheritance was given far too much weight in the content of that and the following courses. I think even my last C++ final project still had using inheritance as a design requirement, and by that point the things the projects were about weren't really great fits for inheritance so you had to just shove it in anyways.
To me it seems exactly like how implementing "uint32_t factorial(uint32_t x)" is a GOD AWFUL practical example of when to use recursion but a FANTASTIC way to introduce what a recursive function is, because that's exactly how you already learned about it in math classes. In the same way, once recursive functions are introduced with a basic understanding of what it is, it's good to move to more practical examples and poke at "why was that actually not a great way to implement factorial but it was great for traversing trees or..." type exploration/questioning.
If in a course example Dog extends Animal, it can be an arbitrary demonstration of language mechanisms (with an uncontroversial is-a relationship) but even in that case it is implicitly suggested that it is a good or "normal" design, implying an alarmingly complex program that has good reasons to deal with those two types.
Such a program is usually not described for brevity, giving the false impression that it exists: if the problem were analyzed with any diligence, usually Dog would appear a completely pointless complication.
I.e. planting a terrible idea is alright so long as by the end the terrible idea was able to be replaced down the line in less time than trying to learn everything "correctly" from the get go. The latter part is where I felt the class failed, it held on to bad idea through the end instead of quickly replacing it with the "next level" of conceptual thinking.
For example, in an introductory physics course teaching Newtonian dynamics without the brutal complications of special relativity and general relativity is fine because it doesn't take much to explain that it is an approximation and it is good enough for "everyday" situations. Students are aware that a better model is available: worst case, they try to get away with not using it.
On the other hand in an introductory programming course teaching that if you have Animals in the program the dog instances "should" belong to a Dog subtype is logically consistent and elegant; the only opposing force is the abstract and uncool engineering principle of keeping software simple, and many teachers are dogmatic and enthusiastic.
It's an interesting, if dated, read.
As they say, "Those who can do. Those who can't teach".
I've never once reached for it for building servers.
For games? All the time.
Hmm, but game objects is exactly the popular use case where traditional inheritance breaks down first and composition makes much more sense? That's why the whole Entity-Component idea came up decades ago (like in Unity, long before the more modern column-database-like ECS designs).
Even in ECS's you will often find inheritance. Some implementations have you inherit a Component struct, others will have the systems inherit a System class.
I'm sure it's still used today in some engines and by some developers but the overwhelming opinion is that doing something like Entity -> Actor -> Monster -> Orc -> IceOrc is a bad idea. Instead it would be like
class IceOrc : Entity { components {Health, Enemy, PlayerSeek, Position, etc} }
Where each component is like class Health : Component { value = 100 }
And yeah, they favour composition re. Components, it's just that the components tend to inherit from a Component class. But I would still call it composition!Like an Entity might have a Position, a reference to the World, the Screen, methods for interacting with other entities, etc. You don't get that from simply implementing an interface, although it's not difficult to pass those into the object. A common example I've seen is having every class extend an EventEmitter superclass. You could implement that as part of the interface but that becomes a ton of duplication.
I think of it like this: If you model your domain as something like `A : B : C : D {}` you get all the problems of inheritance, when simply doing D { A; B; C; } gives you the same benefits without the problems. Doing `A : X {}, B : X {}, C : X {}` sidesteps most of the problems with inheritance but gives you some of the benefits as well.
Inheritance is actually useful for Widget > Input > TextBox since methods and behaviors do follow parent-child and even sibling relationships.
But there aren't many domains like this.
Rust and other languages choosing traits and type classes instead of strict species-oriented class inheritance seems like the much more modern and more widely applicable approach.
Classes feel clinical and dated.
But I do like classes - you can use them without inheritance, and the other stuff that comes with them (encapsulation, polymorphism, etc) fits my mental model. [0] Classes are just syntactic sugar over closures at the end of the day.
But inheritance is best when it's limited and shallow.
0:
The venerable master Qc Na was walking with his student, Anton. Hoping
to prompt the master into a discussion, Anton said "Master, I have
heard that objects are a very good thing - is this true?" Qc Na looked
pityingly at his student and replied, "Foolish pupil - objects are
merely a poor man's closures."
Chastised, Anton took his leave from his master and returned to his
cell, intent on studying closures. He carefully read the entire
"Lambda: The Ultimate..." series of papers and its cousins, and
implemented a small Scheme interpreter with a closure-based object
system. He learned much, and looked forward to informing his master of
his progress.
On his next walk with Qc Na, Anton attempted to impress his master by
saying "Master, I have diligently studied the matter, and now
understand that objects are truly a poor man's closures." Qc Na
responded by hitting Anton with his stick, saying "When will you
learn? Closures are a poor man's object." At that moment, Anton became
enlightened. cloj := func() {
val := 1
return func(m string) {
switch m {
case "inc": val = val + 1
case "dec": val = val - 1
}
}
} ()
cloj("inc")
In JS you can close upon state and return an object, which is essentially how classes work in the language. let cloj = (() => {
let x = 0; return { inc: () => x++, dec: () => x-- }
})()
cloj.inc()At the language implementation level, Scheme-like closures only have one operation, which is the ability to apply them to arguments and thus evaluate them. But as a couple of the other replies show, a closure's code can be implemented as a dispatcher which handles multiple methods. That's not a language feature, just user code.
I wrote the koan quoted above in 2003, a few years before Graydon Hoare started working on Rust. At the time, there weren't any "static" languages with built-in support for closures.
Even Python had only had read-only closure support for a couple of years at that point - so those closures couldn't be used to implement mutation of closed-over variables. (Python didn't get mutable closures until around 2008.) Java only got closure support in version 8 around 2014. C++ 11 got closures in 2011.
Of course you can implement dynamic-dispatching objects using closures in Rust or C++, but that's not going to be the equivalent of a statically-defined structure.
The principle of least surprise, bad evil and scary techniques in general might be the orthodox, efficient and intuitive for maintainers in context. Templates in GUI frameworks versus business apps, for example.
Somehow, it leaked out and convinced everyone that it is a good thing on its own.
- interface/API based -- this is supported in modern languages via interfaces, traits, etc.
- implementation/code based -- modern languages tend to only support single inheritance; they tend to also support default method implementations on the interfaces/traits
Those are actually mechanisms for achieving polymorphism.
Inheritence inherently (heh) consists of "inheriting" parent class' internals and having ability to extend them. It is basically composition, polymorphism and monkeypatching in a trench coat.
Not even gonna comment on "message passing".
https://rust-for-c-programmers.com/ch20/20_3_rust_s_approach...
If Rust pushes any concept it is the use of traits: https://rust-lang.github.io/book/ch10-02-traits.html