Having programmed in Kawa for over a year now, and having programmed in Guile for some time before, I have some thoughts regarding #Java vs Common #Lisp.

I haven't used Java all that much, and Common Lisp even less, but I think that Guile's #GOOPS system is similar to #CLOS - and likewise, the OO system in Kawa literally is that of Java.

I remember the time when I was making set of GUI widgets for my humanoid robot pose editor. I was using GOOPS for that, and I feel that it was a total mess. Well, it did work, but I don't want to go back to that code.

I was using GOOPS in many other ways, for example - to overload various math operations for matrices and quaternions. The code I wrote back then can still be admired, say, here:

https://github.com/panicz/slayer/blob/master/guile-modules/extra/math.scm

I think it was today that I got enlightened on what I really didn't like about the GOOPS/CLOS approach with generic and multimethods. It may seem like extremely flexible system, but I think that multimethods combined with subclasses are a terrible idea in practice.

Suppose that we have a base class <A>

(define-class <A> ())
(define a (make <A>))

and its two subclasses:

(define-class <B> (<A>))
(define b (make <B>))

(define-class <C> (<A>))
(define c (make <C>))

Now, suppose that I also have a generic method:

(define-method (m (x <A>) (y <A>)) 'aa)

and its two specializations

(define-method (m (x <B>) (y <A>)) 'ba)

(define-method (m (x <A>) (y <C>)) 'ac)

Now, what is going to happen when I invoke

(m b c)

?

I checked it with Guile, and it chose the 'ba implementation. I don't know if that's because it was defined earlier, or because the method specialization is defined to be resolved from left to right - and frankly, I don't care.

Imagine that one programmer wrote the 'ac implementation and had some working code. Now, if another programmer comes and defines the 'ba variant, they would break the existing code. And this is the fundamental problem.

slayer/math.scm at master · panicz/slayer

Contribute to panicz/slayer development by creating an account on GitHub.

GitHub

Interestingly, Kawa co-evolved with Guile, and it attempted to provide a GOOPS-like interface to Java object system. And its manual contains the following remark:

"*Warning:* The current implementation of selecting the "best" method is not reliable if there is more than one method. It can select depending on argument count, and it can select between primitive Java methods. However, selecting between different Scheme procedures based on parameter types should be considered experimental. The main problem is we can’t determine the most specific method, so Kawa just tries the methods in order."

I think this is no accident - because in general there's literally no way to "choose the best method".

Now, what I like about Java is that it took the most important concept from the C language - namely - structures with function pointers - and elevated it to the notion of interface, which is probably the most foundational concept for software architecture.

Two things I don't like about Java are method overloading and the lack of mix-ins. But I like it that it didn't provide support for multiple inheritance.

Most importantly, though, Java taught me to think of objects as "things that have their identities and implement interfaces (and can be type-checked)"

Now, the GOOPS-inspired disguise for Java's object model is a bit weird, but fortunately Scheme has sublime support for syntax extensions, so that I could improve the notation a bit.

Anyway, what I wanted to achieve with all this writing was to irritate some Common Lisp fans by saying that Java's take on object-orientation is actually better than their beloved CLOS, which tries to be extremely general, but has a fundamental flaw in it.

Good night, everybody.

Take that, @jack !
@PaniczGodek @jack so Common Lisp resolves this by explicitly saying that arguments are checked left to right by default. So while both methods might seem equally strong/specialized, the first one wins because the first argument is more specialized. See https://cl-community-spec.github.io/pages/Sorting-the-Applicable-Methods-by-Precedence-Order.html
Sorting the Applicable Methods by Precedence Order (CLCS)

@PaniczGodek I could take this more seriously if you actually used CLOS instead of deciding that an incomplete derived system would be able to give you a complete perspective. Also “structs of function pointers” *is* an interface in C…

@PaniczGodek

I pay my bills working mostly with Java. I understand, at least somewhat, the issue you're dealing with and it does look like a real headache.

But to look at this from another angle, I think Java's annotations and all of the operations and compile-time and runtime behavior modifications the annotations enable are effectively a workaround for the absence of multiple inheritance and dispatch.

And essentially all large Java projects I've worked on (10k lines or larger), proprietary or open, tend to develop deep inheritance hierarchies that become difficult to reason about.

I'm not saying that Kawa or the CLOS approach is better. I can't weigh the relative headaches fairly, I don't have the experience. But I bang into the accidental complexities forced by Java's design decisions all of the time.

@firebreathingduck regarding multiple inheritance, my opinion has evolved.

I was once trying to figure out where the idea of multiple inheritance came from, and what was it motivated with, and I ran into a book titled "Object-Oriented Programming: An Evolutionary Approach" by Cox and Novobilski, and they gave an example of modeling a toy truck as "something that is a truck and a toy simultaneously", and I thought to myself, "what a bullshit".

When I started porting GRASP to Kawa, I initially tried to avoid class inheritance (but I did use interface aggregation), and it mostly worked. Now I see inheritance as just a technique of code reuse, and I feel that indeed, there is no particular value in limiting code reuse.

On the other hand, I think that default methods in interfaces can be seen a bit like this. Also, GRASP is currently around 30k lines, and I never felt any need for things that are usually delivered via annotations (which I suspect is largely owed to the Scheme's macro system).

In either case, I find Kawa truly joyful to work with (expect for the rare moments of running into some compiler bugs), and I am puzzled why aren't there more people using that.

@PaniczGodek

Thanks. I might check out Kawa out of curiosity, but at my current job there's Clojure and I've enjoyed working with that so far.

I think you're right about default method implementations in interfaces. I wonder if that feature had been introduced in Java early on, if annotations would never have been added.

@PaniczGodek CLOS says 'ba as well, because method specialization is indeed resolved from left to right:

https://people.cs.georgetown.edu/~maloof/cltl/clm/node284.html#SECTION003217100000000000000

Adding methods to an existing class and method hierarchy can indeed break existing code. But that can happen with single-argument methods as well. And with single inheritance. I have done it in Smalltalk.

28.1.7.1. Determining the Effective Method

@khinsen can you show an example of such a breakage in a single-dispatch system?

@PaniczGodek Here is a Lisp version of what I did in Smalltalk:

(defclass a () ())
(defclass b (a) ())
(defclass c (b) ())

(defgeneric m (x))
(defmethod m ((x a))
'a)
(defmethod m ((x c))
(cons 'c (call-next-method)))

;; Let's use this:

(m (make-instance 'a))
;; ==> A

(m (make-instance 'c))
;; ==> (C . A)

;; Then add:

(defmethod m ((x b))
(cons 'b (call-next-method)))

;; Surprise:

(m (make-instance 'c))
;; ==> (C B . A)

@PaniczGodek In Common Lisp, (call-next-method) is the equivalent of "super" in Smalltalk: it calls the method implementation for the immediate superclass.
@PaniczGodek What I believe to be the real issue is adding methods to someone else's classes. That's something many languages make impossible or difficult. CLOS makes it easy and even look ordinary, in the sense that no special syntactic effort is required.

@khinsen yeah, but I've found this difficulty to be a hindrance, e.g. not being able to add methods to Java's String class (which is where they should belong)

I like the solution adapted in Dart (I think from C#) of "extension methods", where loading a module can make new methods available for a class (but there are static checks for conflicts etc.)

@PaniczGodek Smalltalk calls them extension methods as well. No checks, but the tooling makes them visible as extension methods, allows searching for them, etc.
@khinsen @PaniczGodek A built-in search mechanism sounds like it could ease some pains with Clojure multimethods

@daveliepmann In Common Lisp, you can use the introspection functions of the MOP (Meta-Object Protocol) to crawl or search methods. Development tools can build on that (my inspector does). I don't know if Clojure has similar mechanisms.

In Smalltalk, code is managed as a database, so searching is a lot easier to do, and all development tools support it.

@PaniczGodek