Live data from Hacker News

Liskov Substitution: The real meaning of inheritance

cekrem.github.io

51–60 of 92 posts

Re: Liskov Substitution: The real meaning of inheritance

#51
post #16

Earlier quoted context omitted.

> Don't ever use inheritance. Instead of things inheriting from other things, flip the relationship and make things HAVE other things. This is called composition and it has all the positives of inheritance but none of the negatives. Bah. There are completely legitimate uses of inheritance where it's a really great fit. I think you'll find that being dogmatic about avoiding a programming pattern will eventually get yo…

If you can use functions in the same way objects are used, there’s no need for visitor objects. There’s a reason why everything is a Lisp. All of the patterns are obvious with its primitives, while higher level primitives like classes, interfaces hide that there’s data and there’s behavior/effects.

Visitor objects are needed when you want, at runtime, to decide what code to execute based on the types of two parameters of a function (regular OOP virtual dispatch can only do this based on the type of one argument, the one before the dot). While you can model this in different ways, there is nothing in "plain" Lisp (say, R7RS Scheme) that makes this particularly simple.

Common Lisp does have a nicer solution to this, in the form of CLOS generic functions. In CLOS, methods are defined based on the classes of all arguments, not just the first one like in traditional object systems. Combined with inheritance, you can implement the whole thing with the minimal amount of code. But it's still an OOP system designed specifically for this.

Re: Liskov Substitution: The real meaning of inheritance

#52

Liskov substitution will not save you. One of the worst cases of inheritance I've ever seen was in a hierarchy that was a perfect Liskov fit -- an even better fit than traditional examples like "a JSON parser is a parser". See https://news.ycombinator.com/item?id=42512629 . The fundamental problem with inheritance, and one not shared by any other kind of polymorphism, is that you can make both upcalls and downcalls w…

I've never seen a case where inheritance was superior to composition with a shared interface. Worst case with composition, it just returns the injected class's method directly. The beauty is that this really shines when you apply the liskov substitution principle.

Re: Liskov Substitution: The real meaning of inheritance

#53

Earlier quoted context omitted.

If you can use functions in the same way objects are used, there’s no need for visitor objects. There’s a reason why everything is a Lisp. All of the patterns are obvious with its primitives, while higher level primitives like classes, interfaces hide that there’s data and there’s behavior/effects.

Visitor objects are needed when you want, at runtime, to decide what code to execute based on the types of two parameters of a function (regular OOP virtual dispatch can only do this based on the type of one argument, the one before the dot). While you can model this in different ways, there is nothing in "plain" Lisp (say, R7RS Scheme) that makes this particularly simple. Common Lisp does have a nicer solution to th…

The Visitor Pattern is one of the ones that actually does not go away when you have CLOS. That is to say the traversal and visitation part of it doesn't go away, just all the boiler plate around simulating the double dispatch, like needing two methods: accept and visit and whatnot.

Like say we want to visit the elements of a list, which are objects, and involve them with a visiting object:

  (mapcar (lambda (elem)
            (generic-fun visitor elem))
          obj-list)
We write all the method specializations of generic-fun for all combinations of visitor and element type we need and that's it.

Importantly, the traversal function doesn't have to know anything about the visitor stuff. Here we have mapcar, which dates back to before object orientation.

Re: Liskov Substitution: The real meaning of inheritance

#54

Earlier quoted context omitted.

Inheritance has no place in production codebases—unless there is strict discipline, enforced by tooling, ensuring calls only go in one direction. This Liskov stuff has zero bearing.

You are conflating two completely separate things. The Liskov principle basically defines when you want inheritance versus when you don't. If your classes don't respect the Liskov principle, then they must not use inheritance. The problems from your story relate to the implementation of some classes that really needed inheritance. The spaghetti you allude to was not caused by inheritance itself, if was caused by peop…

I don't believe in free will. People (including me) are going to do the easiest thing possible given the constraints. I don't think that's bad in any way -- it's best to embrace it. In this view, the tools should optimize for right thing to do aligning with the easy thing.

So if you have two options, one better than the other, then the better way to do things should be easier than the worse way. With inheritance as traditionally implemented, the worse way is easier than the better way.

And this can be fixed! You just need to make sure all the calls go one way (making the worse outcome harder), and then very carefully consider and work through the consequences of that. Maybe this fix ends up being unworkable due to the downstream consequences, but has anyone tried?

> I've seen the same things happen with procedural code - a bunch of functions calling each other with all sorts of control parameters, and people adding just one more bool and one more if, because an existing function already does most of what I want, it just needs to do it slightly different at the seventh step. And after a few rounds of this, you end up with a function that can technically do 32 different things to the same data, depending on the combination of flags you pass in. And everyone deeply suspects that only 7 of those things are really used, but they're still afraid to break any of the other 25 possible cases with a change.

Completely agree, that's a really bad way to do polymorphism too. A great way to do this kind of closed-universe polymorphism is through full-fledged sum types.

> People piggybacking on existing code and changing it just a little bit is the root of most spaghetti. Whether that happens through flags or unprincipled inheritance or a mass of callbacks, it will always happen if enough people are trying to do this and enough time passes.

You're absolutely right. Where I go further is that I think it's possible to avoid this, via the carrot of making the better thing be easy to do, and rigorous discipline against doing worse things enforced by automation. I think Rust, for all its many imperfections, does a really good job at this.

edit: or, if not avoid it, at least stem the decline. This aligns perfectly with not believing in free will -- if it's all genetic and environmental luck all the way down, then the easiest point of leverage is to change the environment such that people are lucky more often than before.

Re: Liskov Substitution: The real meaning of inheritance

#55

Liskov substitution will not save you. One of the worst cases of inheritance I've ever seen was in a hierarchy that was a perfect Liskov fit -- an even better fit than traditional examples like "a JSON parser is a parser". See https://news.ycombinator.com/item?id=42512629 . The fundamental problem with inheritance, and one not shared by any other kind of polymorphism, is that you can make both upcalls and downcalls w…

> No one should ever use inheritance in any long-term production use case without some way of enforcing strict discipline, ensuring that calls can only go one way -- up or down, but not both. I don't know to what extent tooling to enforce this discipline exists. Disagree with your first part. Inheritance used to express Subtyping is different from that used for Code-reuse and yet again different from that used for im…

Thanks for that article -- I have to agree with Jacob Zimmerman in the comments to the article:

> I don’t get it. I read one part of the article, think I get it, then I read a different part and what I read there doesn’t jive with what I thought I understood. And I can’t figure out how to reconcile them.

---

> As for tooling, you have to enforce the contract using pre/post/inv clauses following Meyer's DbC and also explicit documentation.

I think we call them asserts and type-level state machines :)

I don't really believe in documentation as enough of a barrier to doing worse things. It must, at a structural level, be easier to do better things.

Re: Liskov Substitution: The real meaning of inheritance

#56

Earlier quoted context omitted.

> No one should ever use inheritance in any long-term production use case without some way of enforcing strict discipline, ensuring that calls can only go one way -- up or down, but not both. I don't know to what extent tooling to enforce this discipline exists. Disagree with your first part. Inheritance used to express Subtyping is different from that used for Code-reuse and yet again different from that used for im…

Thanks for that article -- I have to agree with Jacob Zimmerman in the comments to the article: > I don’t get it. I read one part of the article, think I get it, then I read a different part and what I read there doesn’t jive with what I thought I understood. And I can’t figure out how to reconcile them. --- > As for tooling, you have to enforce the contract using pre/post/inv clauses following Meyer's DbC and also e…

There is no confusion if you understand that Inheritance is just a "mechanism" to express three (and maybe more) different kinds of "policies" and a single class may implement any or all of them in which case it becomes important to disambiguate which methods/functions express which "policies". There is a abstract concept and a syntactical expression of that concept which needs to be clear in one's mind.

Again, asserts are just the "mechanism" to express pre/post/inv "policies" in code. Without having an understanding of pre/post/inv from the pov of Hoare Logic, merely using asserts will not give you much benefit. Documentation is quite important here.

Both the above can be seen in the design of the Eiffel Language where they are integrated into proper syntactical mechanisms. Once you understand the concepts here, you can apply them explicitly even if your language does not support the needed syntax (eg. Contracts). See Bertrand Meyer's OOSC2 for details - https://bertrandmeyer.com/oosc2/ Specifically "Design-by-Contract (DbC)" and "Inheritance Techniques" and "Using Inheritance well".

Also relevant is my other comment here - https://news.ycombinator.com/item?id=42788947

Re: Liskov Substitution: The real meaning of inheritance

#57

Earlier quoted context omitted.

Thanks for that article -- I have to agree with Jacob Zimmerman in the comments to the article: > I don’t get it. I read one part of the article, think I get it, then I read a different part and what I read there doesn’t jive with what I thought I understood. And I can’t figure out how to reconcile them. --- > As for tooling, you have to enforce the contract using pre/post/inv clauses following Meyer's DbC and also e…

There is no confusion if you understand that Inheritance is just a "mechanism" to express three (and maybe more) different kinds of "policies" and a single class may implement any or all of them in which case it becomes important to disambiguate which methods/functions express which "policies". There is a abstract concept and a syntactical expression of that concept which needs to be clear in one's mind. Again, asser…

I agree that inheritance does too many things and has too many degrees of flexibility. I think other kinds of polymorphism like typeclasses don't have this issue, and are better due to that. Automation is highly preferable to documentation.

I think the discussion would benefit from you concretely working through an example. What change(s) are you proposing to how inheritance is done in C++ or Java, and how would they prevent spaghetti code and nested upcalls/downcalls?

Re: Liskov Substitution: The real meaning of inheritance

#58
post #4

Do yourself a favor and wear yourself off all this SOLID, Uncle Bob, Object Oriented, Clean Code crap. Don't ever use inheritance. Instead of things inheriting from other things, flip the relationship and make things HAVE other things. This is called composition and it has all the positives of inheritance but none of the negatives. Example: imagine you have a school system where there are student users and there are…

Inheritance is nothing more or less than composition + implementing the same interface + saving a bit of boilerplate. When class A inherits from class B, that is equivalent to class A containing an object of class B, and recoding the same interface as class B, and automatically generating method stubs for every method of the interface that call that same method on your B. That is, these two are perfectly equivalent:…

Amen.

Reading the comments from top to bottom, the above is exactly what I wanted to write.

Re: Liskov Substitution: The real meaning of inheritance

#59
post #48

Liskov substitution will not save you. One of the worst cases of inheritance I've ever seen was in a hierarchy that was a perfect Liskov fit -- an even better fit than traditional examples like "a JSON parser is a parser". See https://news.ycombinator.com/item?id=42512629 . The fundamental problem with inheritance, and one not shared by any other kind of polymorphism, is that you can make both upcalls and downcalls w…

Can constructors and destructors still go counter to this idea if needed?

I think values should generally only be combined into a structure at the end (no half-formed structures with null data, no calls on methods that work on half-formed structures).

Destructors are more complicated, there's definitely times where you have to violate invariants that otherwise are always the case.

Re: Liskov Substitution: The real meaning of inheritance

#60

Earlier quoted context omitted.

Visitor objects are needed when you want, at runtime, to decide what code to execute based on the types of two parameters of a function (regular OOP virtual dispatch can only do this based on the type of one argument, the one before the dot). While you can model this in different ways, there is nothing in "plain" Lisp (say, R7RS Scheme) that makes this particularly simple. Common Lisp does have a nicer solution to th…

The Visitor Pattern is one of the ones that actually does not go away when you have CLOS. That is to say the traversal and visitation part of it doesn't go away, just all the boiler plate around simulating the double dispatch, like needing two methods: accept and visit and whatnot. Like say we want to visit the elements of a list, which are objects, and involve them with a visiting object: (mapcar (lambda (elem) (gen…

The traversal is not really part of the visitor pattern. The element.accept(visitor) function together with the visitor.visitElementType(element) are the identifying part of the visitor pattern, and they completely disappear with CLOS.

A classic example is different parsers for the same set of expression types. The expressions likely form a tree, you may not need a list of expressions at all, so no mapcar.

Post reply on HN