Live data from Hacker News

Liskov Substitution: The real meaning of inheritance

cekrem.github.io

71–80 of 92 posts

Re: Liskov Substitution: The real meaning of inheritance

#71

Earlier quoted context omitted.

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.

The motivating scenario for the Visitor pattern is processing an AST that has polymorphic nodes, to achieve different kinds of processing based on the visiting object, where special cases in that processing are based on the AST node kind.

Even if we have multiple dispatch, the methods we have to write for all the combinations do not disappear.

Additionally, there may actually be a method analogous to accept which performs the recursion.

Suppose that the AST node is so abstract that only it knows where/what its children are. Then you have some:

  ;; accept renamed to recurse; visitor to fun
  ;; visit is funcall

  (defmethod recurse ((node additive-expr) fun)
    (recurse (additive-left-child node))
    (recurse (additive-right-child node))
    (funcall fun node))
(We might want recurse-bottom-up and recurse-top-down.)

If all the AST classes derive from a base that uniformly maintains a list of n children, then this would just be in the base: (for-each-child ch (recurse ch fun)) or whatever.

Suppose we don't want to use a function, but an object (and not to use that object as funcallable). then we need (let's integrate the base class idea also):

  (defmethod recurse ((node ast-node-base) agent)
    (for-each-child ch (recurse ch action-object))
    (do-action node agent))   ;; basically (visit node visitor)
Now we have a myriad method specializations of do-action.

  (defmethod do-action ((node additive-expr) (agent printer))
    ...)

  (defmethod do-action ((node if-statement) (agent printer))
    ...)

By doing it this way, we get rid of a lambda shim. Instead of:

  (let ((printer (make-printer *std-output*)))
    (recurse ast (lambda (node) (do-action node printer))))
we can just have:

  (let ((printer (make-printer *std-output*)))
    (recurse ast printer))
It's only not the Visitor Pattern because I used recurse instead of accept, do-action instead of visit, and agent instead of visitor.

Re: Liskov Substitution: The real meaning of inheritance

#72

A problem that only exists in OOP codebases. Just don't do it and avoid the issue entirely.

What do you work on that avoids oop code bases?

I've written Rust full time for the last 8 years, being part of teams that have shipped several large, transformative, and basically correct projects. No OOP in sight. It's wonderful!

Re: Liskov Substitution: The real meaning of inheritance

#73
post #28

Earlier quoted context omitted.

I will agree that the problem that class hierarchies attempt to solve is a problem one usually does not really have, but in your example you have not solved it at all. It matches a relational database well, but once you have a just a user reference, you can not narrow it down to an employee without out of band information. If a user should be just one type that can be multiple things at once, you can give them a set…

I agree that there are better ways to model roles and FGA the point of the example wasn't to be an idiomatic solution to that problem, but to illustrate the pointlessness of inheritance in general, and User Student Employee was the first thing that came to mind that was more "real" than the usual Animal examples in any case, as for only having a User reference, you don't ever have only a User reference - overwhelming…

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 variations, in many cases.

> and each of them would have those attributes on them that characterize each of them respectively (and those attributes would not be shared - if they were, such shared attributes would go on the User object)

Suppose you want to compute a price for display, but its computation is different for Students and Employees. The display doesn't care if it's a Student or an Employee, it doesn't want to need to know if it's a Student or an Employee, it just wants the correct value computed. It can't just be a simple shared attribute. At some point you will need to make that distinction, whether it's a method, a conditional, or some other transform. It's not obvious how your solution could solve this more elegantly.

> which is insane

Not at all, in my view this kind of pattern matching is generally preferable over dynamic dispatch with methods, because you see what can happen in all cases, at a glance. But again, you do not even have a contrived example where you would need dynamic dispatch in the first place, so naturally its use not obvious.

Re: Liskov Substitution: The real meaning of inheritance

#74
I don't feel the rectangle/square example is valid, given that both alternatives follow different designs - there's no Shape base class in the inheritance example. Moreover, I don't think that switching from a base (abstract) class to an interface is enough on itself to call it composition.

The two issues the article mentions have imho less to do with the LSP itself, and more with the limitations that different programming languages have when it comes to define contracts through interfaces (not the same thing), like the lack of exception specs or non-nullability enforcement.

Re: Liskov Substitution: The real meaning of inheritance

#75
post #28

Earlier quoted context omitted.

I agree that there are better ways to model roles and FGA the point of the example wasn't to be an idiomatic solution to that problem, but to illustrate the pointlessness of inheritance in general, and User Student Employee was the first thing that came to mind that was more "real" than the usual Animal examples in any case, as for only having a User reference, you don't ever have only a User reference - overwhelming…

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. 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. There are so many good reasons not to want to do this I don't even know where to begin. 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. Also, such "reusable" if this then do this else do that methods when allowed to propagate through the stack means every function is no longer a function, it's actually *n functions, so ease of grokking and maintainability goes out the window. It's much better to relegate such ifs to the edges of the system (eg the http resource endpoint layer) and from there call methods that operate on what you want them to operate on, and do not require ifs.

Re: Liskov Substitution: The real meaning of inheritance

#76
post #34

Earlier quoted context omitted.

I love your concrete examples! Thanks for sharing the pointer to your wasm engine. Is that part of a course you teach, or something born out of an auto-didactic pursuit?

User "titzer" is Ben Titzer; co-founder of WebAssembly - https://s3d.cmu.edu/people/core-faculty/titzer-ben.html

TIL, thank you!

Re: Liskov Substitution: The real meaning of inheritance

#77

Earlier quoted context omitted.

I am not sure that you understood what i wrote. Inheritance's flexibility is its very strength that allows you to express different concepts elegantly. Also no amount of Automation/Tooling/etc. can substitute for documentation explaining the intent behind the code. The main thing i would like to see in C++/Java/whatever is support for "Design-by-Contract" (DbC) similar to that given in Eiffel. There is already a prop…

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 of the largest and commercially most successful class library and application framework (i.e. MFC) where you can see classic OOD/OOP (including upcalls/downcalls) in action; and yet you say i am "speaking in generalities"! It seems you are not willing to read/study but expect a couple of paragraphs to enlighten everything, which is not going to happen.

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?

> 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

It is not up-in-the-air when ideas and specific books by authors like Bertrand Meyer and Barbara Liskov (both researchers and practitioners) are being pointed out. Trying to simplify their concepts into a couple of paragraphs would invariably miss important nuances and lead to misinterpretations (the bane of most HN discussions based on trivial articles/blog posts). Hence it is better they are studied directly and then we can have a discussion if you would like.

> Meanwhile, users suffer in ways that just don't happen with typeclass-based polymorphism, and none of this discourse is required in my world. So why should I not recommend everyone use typeclass-based polymorphism?

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.

> No, being too flexible is a weakness, not a strength. At scale, rigorous discipline enforced by tooling is required.

Flexibility increases your "design space" and hence never a weakness. Rigorous discipline is needed throughout development but tooling can only do so much.

> In particular, invariants like "no downcalls" or "no upcalls" should 100% be enforced by automation.

This depends on the concept you are trying to express and cannot be the same in all scenarios (except for direct ones like "interface implementation").

> I'd rather not?

Well, you did ask for a concrete example and i showed you MFC apps.

> This sounds really bad to me at scale and under pressure.

Saying something is "bad" or "spaghetti" without understanding the design concepts behind the implementation is wrong. MFC is one the largest and most successful application frameworks in the industry and has proven itself in all sorts of applications at scale; studying it teaches one lots of OOD/OOP techniques (good/bad/ugly) needed in real-life industry apps.

Re: Liskov Substitution: The real meaning of inheritance

#78
post #76

Earlier quoted context omitted.

User "titzer" is Ben Titzer; co-founder of WebAssembly - https://s3d.cmu.edu/people/core-faculty/titzer-ben.html

TIL, thank you!

Yeah, there are some real good experts on various subjects here on HN. One thing i would recommend is to contact anybody directly if needed (through their email ids in their profile or otherwise) with any questions you might have. That way you can have a longer discussion and/or learn more on specific subjects. Most people are willing to help generously when approached in a knowledge-seeking manner. I always look at HN threads/discussion as merely giving me an idea of different concepts/subjects and ask for pointers to more knowledge either books/papers or experts. Hopefully i also do the same with my comments thus helping the overall s/n ratio of this site.

Re: Liskov Substitution: The real meaning of inheritance

#79

A problem that only exists in OOP codebases. Just don't do it and avoid the issue entirely.

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 overcomplicate things.

The question of if a square is a rectangle or a rectangle is a square is the sort of thing that comes from OOP brain-rot. They're just data, and their "isa" relationship is likely not even relevant to the problem you're actually trying to solve, like displaying them onscreen.

A "square" could be a function that makes a rectangle out of a single float. A "rectangle" could be a function that produces a polygon. The concepts need not be modeled as types or objects at all.

It depends on the actual use case.

Re: Liskov Substitution: The real meaning of inheritance

#80
post #68
post #52

Earlier quoted context omitted.

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.

I think Python's pattern using inheritance for mixins is probably a good candidate. But Python does have a culture of "inheritance is only for sharing code user beware if you try to use it for other things." Python's ABC classes for collections is also a good use of inheritance. Inherit from MutableMapping, implement the required methods, boom you get all the other mapping methods for free. Pydantic / dataclass inher…

Same for Ruby, and you don't even need to inherit. You include the Enumerable module, implement next, and your instance is suddenly iterable
Post reply on HN