Live data from Hacker News

Liskov Substitution: The real meaning of inheritance

cekrem.github.io

81–90 of 92 posts

Re: Liskov Substitution: The real meaning of inheritance

#81
post #75

Earlier quoted context omitted.

One example for when you might want something like a class hierarchy is something like a graph/tree structure, where you have lots of objects of different types and different attributes carrying references to each other that are not narrowly typed. You then have a set of operations that mostly do not care about the narrow types, and other operations that do care. You have things that behave mostly the same with small…

Your price could simply be an interface that both Student and Employee implements, and at the call site, you simply call foo.calculatePrice - Student will calculate it one way and Employee another, there is zero need for inheritance, or for the call site to know which object is which of the two types - all it needs to know is it has the calcultePrice interface. I also prefer pattern matching over dynamic dispatch. Bu…

> I also prefer pattern matching over dynamic dispatch. But I maintain that conjuring up attributes out of thin air (which is 100% what you're doing when your function accepts one type, and then inside, conjures up another, that has additional attributes from the input!) is insane.

Does it though? In one case, you have:

    type S = A|B
    func calculate(x:S):number
In the other, you have:

    interface S = {calculate:() => number}
    A::calculate():number
    B::calculate():number
but also implicitly the possible:

    Z::calculate():number
The latter case is more flexible, but also harder to reason about. Whoever calls S::calculate can never be quite sure what's in the bag.

> For one, it's a huge footgun for when you add another type that inherits - the compiler will NOT force you to add new branches to cover that case, even though you may want it to.

Lack of exhaustiveness checks is a problem many languages have, and if that's the case for you, that's an argument for preferring methods.

> it's actually n functions, so ease of grokking and maintainability goes out the window

...but that's exactly what interface methods are: N functions. That's what your problem is. You can't get around it. Moreover, if most of your types do not have a distinct implementation for that method, you will still need to define them all. This is where both the "super-function" and inheritance (or traits) can save you quite a bit of code.

Re: Liskov Substitution: The real meaning of inheritance

#82

Earlier quoted context omitted.

What do you work on that avoids oop code bases?

At work we sadly have to implement very OOP-y standards with all the bullshit that entails. 11 levels of inheritance with overrides all over the place sure isn't fun to deal with. But for things I do myself I use objects and interfaces strictly as a tool to solve specific problems, not as the overall program structure. Most of the time you just need to turn some bits into other bits with a function, no need to overco…

That's an interesting perspective on inheritance.

The problem I see inheritance solving is not having to distribute subtype-specific logic throughout your code base - functions can interact with objects in a generic manner and you get to keep subtype-specific code all in one spot. That's a win.

Inheritance isn't the only means for achieving this capability, though. You can also use interfaces and protocols. I prefer to use interfaces. If my class needs to implement an interface than that's explicit: it implements the interface. I can use inheritance if the class really IS-A variant of another class and it can use that base class in fulfilling its obligation in implementing that interface. That's an implementation detail. But the fact it has responsibility for implementing that interface is made explicit.

Re: Liskov Substitution: The real meaning of inheritance

#83
post #21

Earlier quoted context omitted.

I see no need for inheritance there, that can and should be done using interfaces eg in the contrived example I gave, any of all three of the User, Student and Employee can implement the interface (and if needed Student could simply delegate to its internal User while Employee could "override" by providing its own, different implementation)

What difference do you see between implementing an interface and inheriting from a class, that makes one good and the other bad? I'm asking beyond the arbitrary distinctions that some languages like Java or C# bake in.

Interfaces manifest the object's responsibility. Functions accepting objects as parameters should work with interfaces, not type instances. That way the responsibilities and capabilities are clear to the user of the interface and the implementor.

As far as how the implementor may fulfill its interface obligation, it may use inheritance, if it truly has an IS-A or subtype relationship with the base object.

Re: Liskov Substitution: The real meaning of inheritance

#85

Earlier quoted context omitted.

Sorry I don't think your response really gets to the point. I'm aware of various techniques like contracts, but you're speaking in generalities rather than specifics. So yes, I haven't quite understood what you meant. This is a common frustration I have with OOP discourse, it tends to be really up-in-the-air and not grounded in concrete specifics. (The article you linked also has this issue.) Meanwhile, users suffer…

I pointed you to a specific book i.e. OOSC2 and three specific chapters in it (to start with) which explain the concepts well with examples you asked for. How much more specific can one get? If you already know contracts then it should be easy to translate the concepts to any language of your choice. Meyer provides a thorough rationale and is extremely detailed in his examples. Furthermore, i also pointed you to one…

> Flexibility increases your "design space" and hence never a weakness.

This is just objectively false. Constraints liberate and liberties constrain.

> Rigorous discipline is needed throughout development but tooling can only do so much.

Have you used Rust? I would recommend building some kind of non-trivial command line tool with it — you will quickly see how low your expectations for tooling have been.

> Eg: Base.method() has {pre1} and {post1} as contracts. Derived.method() has {pre2} and {post2} as contracts. What should be the relationship between {pre2}&{pre1} and {post2}&{post1} to enforce proper subtyping?

As someone who understands variance etc quite well, my answer is to simply not have subtypes. You absolutely do not need inheritance subtyping to build production software. (Rust has subtyping and variance only for lifetime parameters, and that's confusing enough.)

> Sure, there are other types of polymorphisms which can be better in certain scenarios. But that is not under discussion here; we are talking about "traditional" dynamic runtime dispatch based polymorphism which is far easier to understand and implement even in small languages like C.

I use traits for runtime dispatch in Rust all the time?

Inheritance is only traditional because C++ and Java made it so. I think it's been a colossal mistake.

Re: Liskov Substitution: The real meaning of inheritance

#86
post #82

Earlier quoted context omitted.

At work we sadly have to implement very OOP-y standards with all the bullshit that entails. 11 levels of inheritance with overrides all over the place sure isn't fun to deal with. But for things I do myself I use objects and interfaces strictly as a tool to solve specific problems, not as the overall program structure. Most of the time you just need to turn some bits into other bits with a function, no need to overco…

That's an interesting perspective on inheritance. The problem I see inheritance solving is not having to distribute subtype-specific logic throughout your code base - functions can interact with objects in a generic manner and you get to keep subtype-specific code all in one spot. That's a win. Inheritance isn't the only means for achieving this capability, though. You can also use interfaces and protocols. I prefer…

Yeah but that's what I mean by using it as a tool, you're not trying to model some weird taxonomy with your classes (like the square/rectangle situation), you're using a language feature to enable generic code and/or for code reuse, any real-world relationship between the concepts is irrelevant.

Inheritance is bad because it enforces a subtyping relationship alongside code reuse and data reuse. It is extremely rare you want all 3 together and even when you think you do there's a rude awakening coming your way, specially if you did the "model real world concepts as a class hierarchy thing."

The object-relational mismatch is a weakness of the object side, not of the relational. Use the right tool for the job and forget about stupid programming paradigms.

Re: Liskov Substitution: The real meaning of inheritance

#87

Earlier quoted context omitted.

I pointed you to a specific book i.e. OOSC2 and three specific chapters in it (to start with) which explain the concepts well with examples you asked for. How much more specific can one get? If you already know contracts then it should be easy to translate the concepts to any language of your choice. Meyer provides a thorough rationale and is extremely detailed in his examples. Furthermore, i also pointed you to one…

> Flexibility increases your "design space" and hence never a weakness. This is just objectively false. Constraints liberate and liberties constrain. > Rigorous discipline is needed throughout development but tooling can only do so much. Have you used Rust? I would recommend building some kind of non-trivial command line tool with it — you will quickly see how low your expectations for tooling have been. > Eg: Base.m…

> This is just objectively false. Constraints liberate and liberties constrain.

You are completely wrong here. Flexibility by definition means an increase in the allowed degrees of freedom in one or more axes which in turn allows one to mix and match feature sets to express more design concepts (eg. Multi-paradigm). Your second line is a silly slogan which presumably means constraints make the job of picking one choice from a set easier due to less thought needed. It is applicable to inexperienced developers but certainly not to experienced ones who need all the flexibility that a language can give.

> As someone who understands variance etc quite well, my answer is to simply not have subtypes. You absolutely do not need inheritance subtyping to build production software. (Rust has subtyping and variance only for lifetime parameters, and that's confusing enough.)

You have not understood the example. Variance is used to constrain types but pre/post are predicates relating subsets of values from the types; this constrains the state space (cartesian product of the types) itself. Second, your statement not to use subtyping is silly. Subtype relationships arise naturally amongst concepts in any non-trivial system which you can group in a hierarchy based on commonality (towards the top) and variability (towards the bottom). Inheritance is just a direct way of expressing it.

> Inheritance is only traditional because C++ and Java made it so. I think it's been a colossal mistake.

Statements like these betray an ignorance of the subject. I have already shown that Inheritance can be used for different purposes of which Subtyping in the LSP sense is what everybody agrees on. The other uses need experience and discipline but are very powerful when done clearly. Inheritance was first introduced in Simula67 based on a idea presented by Tony Hoare in 1966. C++ popularized it and others simply copied it. See wikipedia for more details - https://en.wikipedia.org/wiki/Inheritance_(object-oriented_p...

PS: This discussion reminded me of "The Blub Paradox" by Paul Graham (https://paulgraham.com/avg.html) which i think most Rust evangelicals suffer from. Just from my cursory look at Rust i have seen nothing compelling to want me to study it in depth over my preferred language of C++. With the addition of more features into "Modern C++" to support Functional Programming it has become even more flexible and powerful albeit with a steeper learning curve.

Re: Liskov Substitution: The real meaning of inheritance

#88
post #31
post #24

Earlier quoted context omitted.

I happily admit it's more than possible to come up with examples that make inheritance shine. After all, that's what the authors of these books and articles do. But most of them put the cart before the horse (deliberately design a "problem" that inheritance "solves") and don't seriously evaluate pros and cons or even consider alternatives. Even then, some of the examples might be legitimate, and what you're referring…

I agree that examples matter a lot, and for some reason a lot of introductory OO stuff has really bad examples. Like the whole Person/Employee/Employer/Manager dark pattern. In no sane world would a person's current role be tied to their identity--how do you model a person being promoted? They suddenly move from being an employee to a manager...or maybe they start their own business and lose their job? And who's mode…

> Algebraic data types excel at data modeling.

Any good resources you can point to for this?

Re: Liskov Substitution: The real meaning of inheritance

#89

Earlier quoted context omitted.

> Flexibility increases your "design space" and hence never a weakness. This is just objectively false. Constraints liberate and liberties constrain. > Rigorous discipline is needed throughout development but tooling can only do so much. Have you used Rust? I would recommend building some kind of non-trivial command line tool with it — you will quickly see how low your expectations for tooling have been. > Eg: Base.m…

> This is just objectively false. Constraints liberate and liberties constrain. You are completely wrong here. Flexibility by definition means an increase in the allowed degrees of freedom in one or more axes which in turn allows one to mix and match feature sets to express more design concepts (eg. Multi-paradigm). Your second line is a silly slogan which presumably means constraints make the job of picking one choi…

> Your second line is a silly slogan which presumably means constraints make the job of picking one choice from a set easier due to less thought needed

That is absolutely not what it means, and it is not a silly slogan — it is a basic law of reality.

As an example, if your build system is monadic (build nodes can add new nodes dynamically) then the number of nodes in it is not known upfront. If the build system is not monadic, the number of nodes is determined at the start of the build process.

As another example, the constraints that Rust sets around & and &mut mean that the compiler can do really aggressive noalias optimizations that no one would even dream about doing in C or C++.

See https://m.youtube.com/watch?v=GqmsQeSzMdw for more examples.

> It is applicable to inexperienced developers but certainly not to experienced ones who need all the flexibility that a language can give.

I'm quite an experienced developer, and I've tended to use more constrained languages over time. I love the fact that Rust constrains me by not having uncoordinated shared mutable state.

> This discussion reminded me of "The Blub Paradox" by Paul Graham (https://paulgraham.com/avg.html) which i think most Rust evangelicals suffer from

At Oxide we use Rust and would never have been able to achieve this level of rigor in C++. Hell, try writing anything like my tool https://nexte.st/ in C++ (be sure to get the signal handling exactly right). Rust tooling is at a completely different quality level from earlier-generation languages.

Re: Liskov Substitution: The real meaning of inheritance

#90

Earlier quoted context omitted.

> This is just objectively false. Constraints liberate and liberties constrain. You are completely wrong here. Flexibility by definition means an increase in the allowed degrees of freedom in one or more axes which in turn allows one to mix and match feature sets to express more design concepts (eg. Multi-paradigm). Your second line is a silly slogan which presumably means constraints make the job of picking one choi…

> Your second line is a silly slogan which presumably means constraints make the job of picking one choice from a set easier due to less thought needed That is absolutely not what it means, and it is not a silly slogan — it is a basic law of reality. As an example, if your build system is monadic (build nodes can add new nodes dynamically) then the number of nodes in it is not known upfront. If the build system is no…

Again, these are all your preferences/opinions which you are stating as some sort of acknowledged truth; which is most definitely not the case. While there are many good points about Rust it is quite over-hyped with evangelical zeal which is why a lot of software engineers are turned off of it. Graydon Hoare himself has said he took the good ideas from old languages and put them together. That in itself is obviously not a bad thing (imo, the industry killed research in programming languages/OS from the mid-nineties when Java was marketed up the wazoo by Sun throwing ungodly amounts of money at it) but the "saviour complex" being pushed is a strict no-no with experienced C/C++ developers.
Post reply on HN