Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Polymorphism and other important aspects of inheritance (utcc.utoronto.ca)
54 points by ingve on July 10, 2023 | hide | past | favorite | 36 comments


I wouldn't even call "polymorphism" an "aspect of inheritance" -- you can have polymorphism _without_ inheritance (as a code re-use form), and polymorphism is actually more fundamental and importance than inheritance.

I feel like this "hot take" is actually a pretty common take.

You can have polymorphism without inheritance; you can't really have inheritance without polymorphism. To the extent that inheritance necessarily "includes" polymorphism, so polymorphism is in that sense an aspect -- yeah, it's more important and fundamental.

And I think this is actually a pretty common take. All the takes saying inheritance itself is not fundamental to OO (and even sometimes saying OO is better without inheritance), are variations of saying this.


> you can't really have inheritance without polymorphism.

"private inheritance" which is really composition is inheritance without polymorphism


With nowadays approach where usually people favor composition over inheritance all is true that OO is better without inheritance.


Except that composition doesn't give you polymorphism.

I always though that dichotomy missed the point. You can give-up inheritance for generics, for interfaces (as if those were a different thing), for type classes, or for type ducking; but you can't give-up inheritance for composition because the only thing composition solves is code reuse.


> for interfaces (as if those were a different thing)

Interfaces are a different thing than inheritance, precisely because of what we are talking about. Inheritance is about code re-use, and interfaces are about polymorphism. Interfaces are the polymorphism part of "inheritance", while dropping the code re-use part.

It is true that composition does not give you polymorphism, absolutely.

> because the only thing composition solves is code reuse.

Exactly. Inheritance combines polymorphism and code re-use in one form. If you instead don't have inheritance, than interfaces (or something similar by a different name; or "duck-typing" if you don't have static typing and also don't really want any kind of typing at all) give you polymorphism, so what gives you code re-use? Composition.

If you split the code re-use function from the inheritance function, interfaces plus composition is the (or one very common/obvious) solution.

I don't think we are disagreeing, I mean to show you we are all violently agreeing. :)


Yeah, inheritance is the union of an interface with a composition with some automatic linking between them. Thus I agree they are not literally the same thing.

I do see some value on the "automatic linking" part, but deciding you don't want the complex mixed concept on your idiom isn't any radical idea that will change your life (neither for the better nor for the worse).


> Inheritance combines polymorphism and code re-use in one form.

Not necessarily; see "Interface vs. Implementation (eg. private derivation in C++) inheritance".


WRT inheritance, composition solves the diamond problem. It’s really about decoupling interfaces for portability more than code reuse.

I think the big abstraction problem inheritance solves is overrides. I think there’s a lot of FP cargo culting that overlooks the benefits of the ability to override (when used wisely). But aside from that I’m not sure there’s any reason to think inheritance is any more or less polymorphic than composition for a language with sufficient reflection/introspection support. If you lack type introspection it’s a different story. Maybe I’m missing your point though.


It is not a dichotomy - it is not all or nothing and it is not mutually exclusive - it is not "use composition always and never ever use inheritance".

Favor composition is just that your solution will most likely be better with it. There are still cases where you want inheritance but also most likely you don't want to create class hierarchy with 5 levels and everything inheriting form everything else but maybe have have a base class. So even in single application or microservice you might still have some inheritance and most likely more composition.

Besides composition uses interfaces heavily because you want to use DI with constructor injection and then you can switch implementation even at run time.


You can still have polymorphism without implementation inheritance, which is the problematic aspect of inheritance.

Interfaces are fine.


Interface implementation does though, that was the whole point.

The problem with inheritance is the code reuse mechanism


> you can't really have inheritance without polymorphism.

That's not true. In C++ you get inheritance without (runtime) polymorphism if all methods are non-virtual. It's very common actually.


You can have inheritance without polymorphism if you don’t override any of the inherited behavior?


Polymorphism and inheritance: simple or complex ideas? Or is meaning depending on the lanuage? I really like the following video: OOP principles (and why so much jargon).

The four principles of object oriented programming (what they are and why they are wrong!) https://www.youtube.com/watch?v=YpYLXq4htKY

> Abstraction, Polymorphism, Inheritance and Encapsulation… Yikes! What the heck does all that mean and why should anyone care?

> Programming is full of jargon. Object Oriented Programming takes jargon to a whole new level. But even so, that list of the “four fundamentals of object oriented programming” is missing something important. In this video, I explain what the four principles are. And I then explain why they are making a big deal out of something that is essentially quite simple."

Later in the video:

> I'd say polymorphism is probably a hundred dollar word for a 50 cent idea. These days computer science teachers and writers have extended the meaning of polymorphism to such an extent that it's often hard to figure out what the heck they're trying to describe.


The first step in separating polymorphism and inheritance is understanding that there are several forms of polymorphism. The one that gets lumped in with inheritance is subtype polymorphism, but there's at least two other sorts (not an expert, there may be more?): parametric polymorphism ("generics") and ad-hoc polymorphism (e.g. rust traits, haskell typeclasses). All of these achieve one form of code sharing, which is that, in one way or another, code that only depends on your external interface can be reused with any implementation of that external interface.

The other place you want code sharing is the implementation of that external interface. Inheritance is one well-known way to do this, but composition/delegation is another strategy to achieve the same thing. Golang's embedded structs are a pretty streamlined (but fairly limited) way of achieving this, and e.g. Kotlin has explicit delegation: `class Derived(b: Base) : Base by b`. Languages like e.g. Java (where classes are open by default) push you towards inheritance, while languages like Kotlin (where classes are final by default, and where you have explicit support for delegation) push you towards composition.

A whole lot of confusion comes from most mainstream OOP languages conflating inheritance and subtype polymorphism.


Row polymorphism is another sort which is only concerned with record types and allows functions to operate on a section of a record without losing type information.


Perhaps I'm dense but I'm not sure I understand the author's point.

In C# I could easily write:

  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
  }

  public class Employee : Person  // Employee inherits from Person
  {
    public string Company { get; set; }
  }
Whereas in Go I might have to do this for "data embedding":

  type Person struct {
    Name string
    Age  int
  }

  type Employee struct {
    Person  // This is embedding Person struct in Employee
    Company string
  }
There's nothing stopping me from doing that in C#.

  public class Person
  {
    public string Name { get; set; }
    public int Age { get; set; }
  }

  public class Employee
  {
    public Person person { get; set; }
    public string Company { get; set; }
  }
> I do think it's important for a programming language to separate these aspects from classical inheritance itself

...they are already separated?


The point is that the Golang version is closer to this:

      interface IPerson {
        public string Name { get; set; }
        public int Age { get; set; }

      }
      public class Person : IPerson
      {
        public string Name { get; set; }
        public int Age { get; set; }
      }

      public class Employee : IPerson
      {
        private Person _person;
        public string Name {
          get { return _person.Name; }
          set { _person.Name = value; }
        }
        public int Age {
          get { return _person.Age; }
          set { _person.Age = value; }
        }
        public string Company { get; set; }
      }
Employee conforms to the same interfaces as Person, but achieves this via delegation (the famous "favour composition over inheritance" thing)


What advantages does the Golang version have over inheritance?

Please, note I don't dispute the "prefer composition over inheritance" thing. In fact, I heartily agree with it.

But in this specific case, where every sinle detail of Person is publicly exposed in Employee (no pun intended), I don't see a material difference. If anything, the example uses composition as a tool to implement inheritance on top of.

Am I missing something?


None of those Person/Employee examples are pure OOP, FWIW.

* https://wiki.c2.com/?AlanKaysDefinitionOfObjectOriented

* https://wiki.c2.com/?AlanKayOnMessaging

"OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme LateBinding of all things." ~ Alan Kay

"The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be." ~ Alan Kay

The antithesis of OOP is exposing the name and age of a Person to the greater system at large.


Bringing up Alan Kay's definition of OOP is approximately worthless when it comes to discussions of mainstream OOP. Yes, Erlang is "more OOP" than Java by Kay's definition. But that ship has sailed. People will (justifiably) look at you funny if you claim your functional language is OO, and when asked to name an Object Oriented language will say C#, Java, or maybe C++.

That definition is dead. Wish it well, then bid it farewell. "OOP" means "Java" and we can just call Erlang and co "Agent Model".


I think reread the first paragraph about Go embeddings and method promotion to get a better idea of what he means by embedding. It sounds more record extension, so your C# example doesn't have the property he's talking about.


This is correct. The last example of adding a Person property to Employee isn’t the same as Go’s struct embedding where Employee gets the fields and structure of Person.


Two things I've learned over 30 years of programming, but mostly the political landscape of software engineering in general are:

1. The code the is used to implement languages or the main "library" of said languages and your code to implement products don't have to and probably should NOT mirror each other.

Other ways to think about it: Just because Java standard library has a generic List -> LinkedList or ArrayList etc..., doesn't mean your software related to managing Patients does NOT mean you need this complication:

Human <- BillablePerson <- Patient (this is somewhat whacky).

I often see developers do things like this because its "a best practice" and maybe there's a 1% of your code that will take advantage of something like that. In reality it makes a lot more sense to just generate a Bill and send it to an Patient. Whether they are a BillablePerson or not doesn't really matter. Anyone opening the user interface can see a payment screen, right? You can flip the status on a Bill to paid, right?

Yes the pedants will come running. And yes if you're app is a framework for medical billing software (you're not a billing company but a company that makes billing frameworks) _maybe_ this is useful? But my answer to that is - even in that case, more and more we're outsourcing screens and concepts to other cloud systems. The concept of polymorphism is neat to iterate a LinkedList and an ArrayList, but it's not buying much for an invoice that gets paid in freshbooks and a patient managed in some EHR software.

So as much as it's nice to "wrap all the things" the software we write is increasingly outsourced to yet another API call.

Last two companies I worked at the config.json file was 50 lines, almost 90% of which is API tokens to various cloud SaaS vendors that take care of billing or email or storage or uploads.

Wrap all the things? It's just not feasible anymore.

2. Second thing I've learned: Don't react to Everything.

Have a bad deployment? Have some requirements that were lost? You can definitely implement a process or two especially for repeating issues. But don't react to everything. If you make a new policy or process or 2-step program for any frown you generate, you'll never get back to coding up the solution. You'll drag your software developers through red tape.


> does NOT mean you need this complication:

> Human <- BillablePerson <- Patient (this is somewhat whacky).

Whenever I see something like this I push the implementor to point me to some code that accepts a "Human" as a parameter and actually does something with it (other than casting it down to Patient). If they can't, I try to encourage them to drop the unneeded hierarchy.


Unless Human already existed and you want to inherit the functionality without retyping stuff or delegating. I think that's where the old "prefer delegation over inheritance" advice comes in but I'm not against convenience-inheritance.


Right - in which case, Human must already be used (by itself) somewhere else.


> Other ways to think about it: Just because Java standard library has a generic List -> LinkedList or ArrayList etc..., doesn't mean your software related to

> managing Patients does NOT mean you need this complication:

> Human <- BillablePerson <- Patient (this is somewhat whacky).

Also: class hierarchies don't have to match real-life hierarchies. Often I find it more useful to implement hierarchies that make the program flow and algorithms work more smoothly and accept hierarchies that don't mirror anything in the physical world.


Yeah I think people get far too carried away with object ontology. You shouldn't be thinking of program objects as direct analogues for real-world things. At most, they're data which describes a real-world object, and like all data, you can arrange it however is most convenient. Normalize it, denormalize it, version it and make it immutable—whatever suits your needs best.


For this type of discussion I think it's important to contrast statically (or manifest) and dynamically typed languages. Lumping together inheritance and polymorphism kind of just happens with the former. With dynamically typed languages you have more freedom to just throw the right functions into your object and informally declare that your object will work with calling function, regardless of the specific type.

Of course now you have to talk about semantic polymorphism, e.g. will the outcome of calling some polymorphic functions have the right effect on the system? This is where static type systems have it easier because you typically tie stuff together in related hierarchies.

The Go structure embedding thing was a surprise to me, having grown up with C and C++ it looks unexpected. Not sure I like it but I think I do.


You're describing nominal and structural typing. This concept is orthogonal to static and dynamic type checking.

Go and TypeScript are examples of languages with statically checked structural type systems. While C# has statically checked nominal type system.

I don't use dynamic languages much, so maybe someone else could chime in with a few examples.


The Oberon programming language supports inheritance and/or polymorphism on different levels of abstraction:

http://miasap.se/obnc/data-abstraction.html


Note that plan9 and Microsoft compilers (and now clang with the right flag) allow the C structure embedding as well, see <https://9p.io/sys/doc/comp.html>, search for “Extensions”.


I think GCC also supports this for a long time already, see -fplan9-extensions and -fms-extensions



Clojure has spent a considerable amount of design work on polymorphism: https://clojure.org/about/runtime_polymorphism




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: