Hacker News new | ask | show | jobs
by kazinator 510 days ago
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.

1 comments

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.