In short, the solution is to define an interface / typeclass / trait / protocol that factors out the particular kind of operation, and separates it from any specific class or data type. Basically it allows to tag any particular data representation with a trait, and then offer an implementation of that trait, without touching any pre-existing code related to that data representation.
In languages that do not allow that (e.g. Java), one has to implement it by hand, using a Visitor pattern. Instead of relying on the language to do the dynamic dispatch, one has to explicitly add a visiting method for another data type.
To me, the turn towards interfaces / typeclasses / traits / protocols is one of the most notable bits of progress in the newer crop of programming languages (and, importantly, their standard libraries).
That just rotates the problem 90 degrees - now adding new functions is easy, but adding new data is hard (requires rewriting every use site).
I really recommmend giving this a read: https://craftinginterpreters.com/representing-code.html#the-...
You should read the linked article to the end (or maybe you did but missed it)
The problem as explained in what you linked:
> an object-oriented language wants you to orient your code along the rows of types. A functional language instead encourages you to lump each column’s worth of code together into a function.
The point was that the final protocol implementation in Clojure solves this. There is no more lumping. The whole thing is freeform. You can extend an existing record (say it's coming from a library or somewhere outside your control) and add interfaces. Or you can make a new record and implement all the existing interfaces