Hacker News new | ask | show | jobs
by titzer 510 days ago
> 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 you twisted up in other ways.

Inheritance can be used in a couple of ways that achieve a very-specific kind of code reuse. While I went through the early 2000's Java hype cycle with interfaces and factories and builders and double-dispatch and visitors everywhere, I went through a period where I hated most of that crap and swore to never use the visitor pattern again.

But hey, within the past two years I found an unbeatable use case where the visitor pattern absolutely rocks (it's there: https://github.com/titzer/wizard-engine/blob/master/src/util...). If you can come up with another way by which you can deal with 550 different kinds of animals (the Wasm instructions) and inherit the logic for 545 and just override the logic for 5 of them, then be my guest. (And yes, you can use ADTs and pattern-matching, which I do, liberally--but the specifics of how immediates and their types are encoded and decoded just simply cannot be replicated with as little code as the visitor pattern).

So don't completely swear off inheritance. It's like saying you'll never use a butter knife because you only do pocket knives. After all, butter knives are dull and not good for anything but butter.

2 comments

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.

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.

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.
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 to might be a case of one. (though I doubt there's no equally elegant and succinct way to do it without inheritance)

But none of that changes the fact that inheritance absolutely shouldn't be the default goto solution for modeling any domain it has become (and we are taught to understand it as)

or that it's exceedingly uncommon to come across situations like yours where you have 500+ shared cases of behavior and you only want to "override" 5

or that inheritance is overwhelmingly used NOT for such niche edge cases but as a default tool to model even the most trivial relationships, with zero justification or consideration

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 modeling these people and what for? That's never shown. Are we the bank? The IRS? An insurance company? Because all of these have a lot of other data modeling to do, and how you represent the identities of people will be wrapped up in that. E.g.--maybe a person is both an employee and a client at the same time? It's all bonkers to try to use inheritance and subtyping and interfaces for that.

Algebraic data types excel at data modeling. It's like their killer app. And then OO people trot out these atrocious data modeling examples which functional languages can do way better. It's a lot of confusion all around.

You gotta program in a lot of different paradigms to see this.

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!
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.
> Algebraic data types excel at data modeling.

Any good resources you can point to for this?