38 pointsby zahrevsky5 days ago8 comments
  • xliia day ago
    After my adventures with different languages I'm on the hill that Rust traits, Haskells typeclasses, (sic!) Go's interfaces, etc. are state-of-art features in modern programming languages.

    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

    • Yes I agree with you. The pattern is "composition" vs "inheritance". Defining a "thing" as "what it can do" instead of "what it is". Instead of saying that "a duck is a Bird which in turn is an Animal which in turn is a LivingThing" (Duck -> Bird -> Animal -> LivingThing) you focus on what a duck can do: a duck "quacks, swims, etc":

          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.
    • gf000a day ago
      I don't see what go's interfaces have to do with the first two. It's just structural typing, isn't it?
      • spoilera day ago
        I think one of those pedagogical half-truths useful for onboarding people onto an idea across different languages or getting parts of a point across, and they cover some similar use-cases.

        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!

    • setopta day ago
      I really miss explicit support for type classes in languages that I use regularly like Python.
  • misja111a day ago
    I see the author fighting really hard against a paradigm that was already well known to be suboptimal 20 years ago .. "Favor composition over inheritance" is a phrase that became common knowledge in e.g. the Java world half way during the 2000's.

    It was even already mentioned in the famous Design Patterns book from the GOF in 1994.

    • goatlovera day ago
      Can't help but wonder if that isn't more the fault of Java's OOP design. Favoring composition over inheritance doesn't seem like something Smalltalkers and Rubyists had to worry as much about.
      • ch_123a day ago
        The GoF book predates Java, and many of the examples in the book are in Smalltalk - so I think it's quite likely that composition over inheritance came from the Smalltalk community.

        Problems with inheritance, such as the fragile base case problem, are intrinsic to inheritance, and not any specific language's implementation of it.

      • hosha day ago
        Ruby does have language features for composition, and “composition over inheritance” has popped up in the community. I have also looked into the use of traits in Smalltalk.
        • lloekia day ago
          Absolutely, usage of inheritance is (comparatively to Java/C++/Python) quite rare in Ruby.

          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.

      • misja111a day ago
        It was definitely also a fault of initial OOP in Java. Java's interfaces became a bit more flexible later on.
        • wk_enda day ago
          What's this referring to?
  • I disagree that OOP tries to be intuitive. I'd say the idea of object-oriented programming is to have better composability. Sure, there are other explanations, especially in textbooks. But the underlying problem OOP tries to solve is composability and reusability of software parts.
  • zozbot234a day ago
    It's worth noting that you can replicate implementation inheritance pretty much as-is in Rust by using the generic typestate pattern. This is effectively what OP calls "policies" except that the policies can also bundle data of their own, just like our derived classes of old; they aren't just limited to picking some statically-known behavior at compile time.

    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.

  • azangrua day ago
    Pardon my ignorance, both about the history of computing and about Rust, but weren't the "new" keyword, and the term "instance" introduced in object-oriented languages? If yes, isn't it deceptive then that Rust has the "new" method, and that the Rust book describes it as creating an instance?
    • aw1621107a day ago
      > If yes, isn't it deceptive then that Rust has the "new" method, and that the Rust book describes it as creating an instance?

      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.

  • barrenkoa day ago
    If I squint a little, this is similar to https://fsharpforfunandprofit.com/.
  • jqpabc1235 days ago
    Inheritance may have some limited utility but it often degenerates into "spaghetti code" dressed up to appear modern and acceptable.

    Just like "goto" in the days of old, it is best used sparingly.

    • WCSTombs5 days ago
      There's a now well-known guideline that says "prefer composition over inheritance." When I saw that, it clarified a lot of things. While much of software design can theoretically be approached in terms of class hierarchies, that's rarely the right place to begin, since after you start preferring composition, inheritance is mostly just a tool to achieve polymorphism.

      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).

      • zamadatixa day ago
        I just went through a CSCI degree program after a long time in the field (figured I'd have some fun + try to catch a few things I may not have had a chance to mess with much over the years, like databases) and I gave myself a pretty decent lean on C++ programming courses where I could.

        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.

        • HelloNursea day ago
          Meaningless toy examples have the problem that they can appear to make sense, planting the seeds of terrible ideas.

          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.

          • zamadatixa day ago
            More or less, this is the https://en.wikipedia.org/wiki/Lie-to-children debate. It'd be nice to always be able to learn the best known things up front, it's not usually a particularly practical approach to learning a complex field.

            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.

            • HelloNurse10 hours ago
              The problem is that treating the incorrect simplification as good can be very tempting for teachers.

              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.

      • riffraffa day ago
        IIRC "Object Oriented Software Construction" by Bertrand Meyer makes it a point to argue against common inheritance examples (Rectangle < Shape, Employee < Person, etc), while still arguing for the utility of (multiple!) inheritance.

        It's an interesting, if dated, read.

      • jqpabc1234 days ago
        did a lot of damage to impressionable programmers.

        As they say, "Those who can do. Those who can't teach".

        • tonyedgecombea day ago
          I always disliked that phrase. In my experience one of the best ways to check whether you understand a subject fully is to teach a course on it.
    • llmslave2a day ago
      Inheritance is great, just depends how you use it.

      I've never once reached for it for building servers.

      For games? All the time.

      • flohofwoea day ago
        > 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).

        • llmslave2a day ago
          Entity systems still rely on inheritance as you typically have an Entity baseclass that all entities derive from. That's just a single level of inheritance. Unity does this through MonoBehavior. Unreal is more inheritance-heavy but developers typically subclass something like `Actor` to one level of additional inheritance beyond the Engine's subclasses. A lot of engines will have multiple superclasses behind a Entity class, but that's an implementation detail of the engine itself and to the game developer, it's treated as a single base class that is typically subclassed a single time for the entity itself.

          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!
          • flohofwoea day ago
            Yes there is some inheritance left in those composition-systems, but a single inheritance level is really more like implementing an interface.
            • llmslave2a day ago
              It is, but you get the benefit of shared behaviour and state.

              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.

      • echelona day ago
        The only two domains where I've felt inheritance is useful are video games and windowing systems.

        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.

        • llmslave2a day ago
          Anything where you have literal "instances" of something with common state/behaviour is prime for inheritance, but that's different from trying to model a domain as a set of hierarchal objects.

          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.
          • ahartmetza day ago
            I guess closures in languages like Scheme can have multiple "methods"? In C++ and Rust, closures (lambdas) are one function with attached state variables. I guess there are ways to attach more callables to a bag of state, but they would involve one lambda for each callable and shared state captured by reference (and good luck dealing with lifecycle issues).
            • llmslave2a day ago
              You can do it multiple ways. Like in go:

                 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()
            • gpderettaa day ago
              a tuple of closures closing over the same object ('let over lambda') is equivalent to an interface.
            • antonvsa day ago
              > I guess closures in languages like Scheme can have multiple "methods"?

              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.

        • bonesssa day ago
          In addition to the underlying domain (and I agree, it’s no coincidence that windowed GUI widgets are common in in OOP textbooks), there’s also overlapping spectrums of language expressiveness and object-orientedness to consider.

          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.

    • bheadmastera day ago
      Inheritance is often used as an enabler for polymorphism in languages that don't support it in any other way.

      Somehow, it leaked out and convinced everyone that it is a good thing on its own.

      • rhdunna day ago
        There are two types of inheritance:

        - 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

        • bheadmastera day ago
          > interfaces, traits, etc.

          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.

        • goatlovera day ago
          It's interesting that Python does support multiple inheritance.
      • goatlovera day ago
        Also encapsulation and code reuse. Message passing if you go by the guy who coined the term.
        • bheadmastera day ago
          Encapsulation is possible without inheritance. And "code reuse" is a vague concept that had nothing to do with inheritance in particular.

          Not even gonna comment on "message passing".

    • atoava day ago
      So it is good that most Rusts introductions tell you that you should favor composition over inheritance in multiple places:

      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

  • 5 days ago
    undefined