A Brief Guide to CLOS (1998)
aiai.ed.ac.uk
aiai.ed.ac.uk
† I took an FP class during my first semester of college. The professor wrote the book himself, and had an extended sidebar comparing the design considerations of OO systems (easy to add another class; tedious to reopen n subclasses to add another method to each one) and FP systems (easy to add another function dispatching on type; tedious to reopen n functions and make them dispatch on a new type). I pointed out that CLOS handled both situations equally well, and in the final edition of the book he tossed in a footnote saying as much and thanking me.
Clojure's multimethods and protocols also solve both.
One is that CL seems to encourage writing everything in CL, so it's easy for library authors to expose everything (like their intermediate AST) to callers. That helps reduce redundancy. In other languages, what I often need is something that's already computed internally, but not exposed in any interface, so I have to build it myself again. A surprisingly common case is a Python or Ruby library where they wrapped a C library to get good performance, and I need data in a C struct that they didn't think anyone would need.
Two is that CL, more than any other language, makes not just for concise code, but for concise diffs. (This is what you mention in your footnote, but I think it's true in general, not just with CLOS methods.) Whenever I wanted to make a change which I could describe briefly in English, it always ended up being something which I could implement in a similarly brief changeset.
Other HLLs can usually, IME, be just as concise for a given solution, but the nearby solutions in the problem space might require a completely different design. This is where the full power of CL helped me. If you know exactly what you want from the start, then you can implement it easily enough in almost any language. If you change your mind halfway through, the weird features available in that "full power" (CLOS dispatch, macros, special vars, etc) makes it possible to avoid rearchitecting the whole thing. (Then you have the choice to gradually refactor to avoid the need for them, or just leave it.)
Despite all the new programming languages in the past ~30 years, I haven't seen any which made diff size a design priority. I think even Clojure isn't quite as good at this, though I'm still a newbie in that language.
You probably spend all day dealing with OO ideas like method dispatch and inheritance, but to see them laid out explicitly in CLOS, as distinct from the underlying Lisp language, really helps you to internalize them and how arbitrary and malleable they are, as well as how astoundingly general they are when interpreted as broadly as CLOS does (the object systems in workaday languages cut off a lot of the avenues open to systems defined in terms of CLOS).
You can get a similar "aha" moment by looking at how Perl does OO if you already know Perl, or reading the implementations of one of its dozens of different user-implemented object systems. Perl has some language-level support for OO but it is very minimal so these object systems have to do most of the work that a Common Lisp object system would.
I think the main benefit is in seeing OO implemented in a language with no built-in support for it, rather than a language built around the concept of classes and inheritance or some version of it.
That said, I used to be fascinated with CLOS, and yet these days I write in Clojure and have no more than a few multimethods in my entire code base. OO has many drawbacks and I find I can write better (simpler, more understandable and debuggable) code without it.
What are some, according to you? I mean, I have read a bit about why immutability and functional programming is becoming popular, e.g. it makes concurrent programming and use of multiple cores easier (only a general idea, may be wrong). But what are your specific reasons for saying OO has many drawbacks?
This realization came as I switched to Clojure: it found that I'd rather use the generic data structures with the excellent functions that come with the standard library. This gave me much better reusability than inheritance. Enforcing contracts can be done in a multitude of ways, and I no longer liked the idea that the only way to enforce data integrity is by keeping the data private and accessible only using "accessors".
Debugging OO code can be a major nightmare. Reading and understanding it is also difficult. How do you know which method implementation will get called at a certain point in your program? You really have no idea until you run it and dynamic dispatch happens. This is also a problem with Clojure multimethods, although it is somewhat mitigated by Clojure's explicit dispatch fn.
As for data modeling, I found that inheritance isn't such a useful tool in practice. It looks great in theoretical problems, but in practice the perfect "is a kind-of" relationship is rare.
The final nail in the coffin was Clojure's STM. Immutable data structures bring so many benefits that after using them for a while it is unthinkable to me right now to go back to mutating data in-place.
>As for data modeling, I found that inheritance isn't such a useful tool in practice.
True. It can lock you into a particular style and hierarchy. Plus, inheritance just to inherit the methods of the superclass seems like overkill or sometimes might be a wrong approach. Personally I think composition can often be more flexible than inheritance.
Interestingly enough, most OO systems for lisp older that CLOS were much more smalltalk like in this sense, so there were several people that found the non-namespaced approach to methods to be superior.
I'm honestly not yet convinced they are superior, but they are clearly more generic, in that it is trivial to implement single-dispatch message-passing style methods in CLOS, but much harder to do the reverse (several patterns in the GoF book exist just to emulate multiple dispatch).
Actually the CLOS combinators show learning from earlier mistakes and allow only :before, :after and :around, the most useful and the most comprehensible combinators.
Lisp Machine Lisp (including the "zetalisp" variant dreamt up by Symbolics' marketing department) had an enormous panoply of method combinators including full control structures: IIRC I had cause to resort at one time or another to :or, :and and :progn combinators (the latter being somewhat like :around, I suppose, but not the same). They made the code extensible, compact, and completely unpredicatable (if you didn't understand the runtime method hierarchy precisely which combined methods would be run in an :or combination?
Nevertheless I sometimes miss them to this day, even in my C++17 codebase!
The CLOS Standard Method Combination has primary, :before, :after and :around methods.
There are in CLOS also various 'simple' method combinations like AND (runs all applicable methods until one is false), OR, + (sums up the results of all applicable methods), PROGN (runs all applicable methods and returns the result of the last one). These seem to be rare in CLOS code.
> They made the code extensible, compact, and completely unpredicatable ...
That's kind of the point that one does not care which methods actually run and when - the 'system' takes care of that. Similar to, say, when you call REDUCE on a list - one does not know which elements are in the list at runtime. Or when you call a generic function on a bunch of instances - you know only at runtime which method runs - because of dynamic dispatch - the method combination feature adds a layer of complication to it. Like on a Lisp Machine where you can add a :before method to a window function and it's immediately active, because the method combination is recomputed. Or when one adds a mixin to a window class and the width of a window then has a different result, because the + generic function computes a new value, because a new method is available via the + method combination. You can scare a lot of people with this, though.
One can program full new method combinations in CLOS, like in Flavors. This might even be useful. ;-) An example is the integration of 'Design by Contract' methodology into CLOS. One can add a DBC method combination and then work with preconditions, postconditions and invariants somehow integrated into Common Lisp's condition system.
https://www.muc.de/~hoelzl/tools/dbc/dbc-intro.html
and newer:
What is the right method for debugging? I've been trying to debug one particular issue in CL code (McCLIM + Climacs) and I've hit the wall in every direction that I tried to see which method was responsible with the issue I've seen.
A plethora of around, before and after in both composition and inheritance that I've lost my way. I've tried various depths of reading and runtime debugging with no significant success. It left me with the image that I should have the whole program in my head to be able to understand it.
It's usually the case that the solution is where you're not looking therefore my question is where (and in what way) should I look at the code to understand what it is supposed to do?
When I was working more with those things I wrote some browsers for browsing methods in CLIM and for MCL. The Lisp Machine should already have some tools, IIRC. LispWorks also has a generic function browser. One enters a generic function and argument types -> LispWorks shows which methods are used for those.
FOO (FLOAT STRING) ->
MULTIPLE-VALUE-PROG1
CALL-METHOD
(METHOD FOO (FLOAT T))
(METHOD FOO (REAL T))
CALL-METHOD
(METHOD FOO :AFTER (T T))I've come to this conclusion during this particular experience and I've confirmed it time and again. I've recently seen an older video of Gregor Kiczales on Aspect Oriented Programming and if I did not read in too much he emphasizes the same issue with proper debugging tools.
Thanks for the tip; it might be what I needed for me to download LispWorks and try again.
[1] Some kernel developers just don't get this even after you show them a call using a function pointer. Read the code they say. Right...
[1] https://en.m.wikipedia.org/wiki/The_Art_of_the_Metaobject_Pr...