summaryrefslogtreecommitdiff
path: root/posts
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2022-10-05 11:15:26 -0400
committerDavid Thompson <dthompson2@worcester.edu>2022-10-05 11:15:26 -0400
commit054a0c432df3224a438126d3d0678f7c32c41746 (patch)
treeefb88bfdd5d31a9b8cd543d14b3638e701a81e0a /posts
parente4d121f8272ad29d6f6fd0dda6f2dfa07fd08663 (diff)
New post about GOOPS.
Diffstat (limited to 'posts')
-rw-r--r--posts/2022-10-05-goops-issues.md278
1 files changed, 278 insertions, 0 deletions
diff --git a/posts/2022-10-05-goops-issues.md b/posts/2022-10-05-goops-issues.md
new file mode 100644
index 0000000..e351ca8
--- /dev/null
+++ b/posts/2022-10-05-goops-issues.md
@@ -0,0 +1,278 @@
+title: Issues with object-oriented programming in Guile
+date: 2022-10-05 11:00:00
+tags: guile, oop
+summary: A list of issues with Guile's OOP system and ways to maybe fix them
+---
+Scheme is often thought of as a functional programming language, but
+really it is a multi-paradigm language, including object-oriented
+programming. My Scheme of choice for the past decade has been
+[Guile](https://gnu.org/s/guile). It comes with support for OOP via
+GOOPS: The Guile Object Oriented Programming System. It's a silly
+name. GOOPS is modeled after the almighty Common Lisp Object System
+or CLOS for short. Overall, it does a good job of adapting the
+concepts of CLOS in a Schemey way. However, most Scheme programmers
+never use OOP, and I think that has left GOOPS a little rough around
+the edges. By mimicking CLOS a bit more, I think GOOPS could make
+someone used to the excellence of CLOS feel a little more at home in
+Schemeland. Also *I* want these features to make my code more elegant
+and less prone to bugs due to hacky workarounds.
+
+## Setter specialization and inheritance do not compose
+
+In Guile, slot accessor specialization and inheritance do not compose.
+You can't specialize an accessor's setter in a parent class and have
+it apply to a child class. The child class defines new slot accessor
+methods specialized for itself, so they come before the specialized
+parent methods in the precedence list.
+
+Example:
+
+```scheme
+(use-modules (oop goops))
+
+(define-class <person> ()
+ (name #:init-keyword #:name #:accessor name))
+
+(define-method ((setter name) (person <person>) new-name)
+ (display "renaming!\n")
+ (slot-set! person 'name new-name))
+
+(define-class <child> (<person>))
+
+(define p1 (make <person> #:name "Alice"))
+(define p2 (make <child> #:name "Bob"))
+
+;; Only the first set! call uses the specialized setter method defined
+;; above.
+(set! (name p1) "Ada")
+(set! (name p2) "Ben")
+```
+
+CLOS does not clobber the method from the parent class:
+
+```lisp
+(defclass person ()
+ ((name :initarg :name :accessor name)))
+
+(defmethod (setf name) (new-name (obj person))
+ (format t "renaming!~&")
+ (setf (slot-value obj 'name) new-name))
+
+(defclass child (person) ())
+
+(defvar p1 (make-instance 'person :name "Alice"))
+(defvar p2 (make-instance 'child :name "Bob"))
+
+;; Both of these setf calls use the specialized setf method defined
+;; above.
+(setf (name p1) "Ada")
+(setf (name p2) "Ben")
+```
+
+As a workaround, each child class can specialize the
+getter/setter/accessor and call `next-method`, but it doesn't feel
+like the way things ought to be. I find the CLOS behavior much more
+desirable. I think this is actually a bug, but I'm waiting on
+confirmation from Guile's maintainers about that before attempting to
+fix it.
+
+## No before/after/around method qualifiers
+
+GOOPS does not support the handy before/after/around method
+qualifiers.
+
+Here's what those look like in Common Lisp:
+
+```lisp
+(defclass person ()
+ ((name :initarg :name :accessor name)))
+
+(defmethod greet ((p person))
+ (format t "You: 'Hello ~a!'~&" (name p)))
+
+(defmethod greet :before ((p person))
+ (format t "> You gather the courage to talk to ~a.~&" (name p)))
+
+(defmethod greet :after ((p person))
+ (format t "> That wasn't so bad.~&" (name p)))
+
+(defmethod greet :around ((p person))
+ (format t "> You take a deep breath.~&")
+ (call-next-method)
+ (format t "> You are socially exhausted. You should rest.~&"))
+
+(greet (make-instance 'person :name "Alice"))
+```
+
+I often want to tack on some logic before or after a method, so these
+qualifiers are extremely useful and they compose nicely with
+inheritance because child classes that implement more specialized
+versions of the base method do not clobber the qualified methods.
+
+With GOOPS, if a parent class wanted to wrap some code around a method
+call, that behavior could only apply to the parent, its ancestors, and
+child classes that *do not* specialize that same method. If a method
+specialized for the child class exists, the wrapper breaks. It's not
+called at all if the more specialized method does not call
+`next-method`, and even if it is called, there is no way to execute
+the body of the more specialized method in the context of the wrapper
+code. With an `around` qualifier, this wouldn't be a problem. To
+workaround this in Guile and preserve the behavior of the previous
+example, a new method `around-greet` needs to be defined, which calls
+`before-greet`, `greet`, and `after-greet`.
+
+```scheme
+(use-modules (ice-9 format) (oop goops))
+
+(define-class <person> ()
+ (name #:init-keyword #:name #:accessor name))
+
+(define-method (greet (p <person>))
+ (format #t "Hello ~a!~%" (name p)))
+
+(define-method (before-greet (p <person>))
+ (format #t "> You gather the courage to talk to ~a.~%" (name p)))
+
+(define-method (after-greet (p <person>))
+ (format #t "> That wasn't so bad.~%"))
+
+(define-method (around-greet (p <person>))
+ (format #t "> You take a deep breath.~%")
+ (before-greet p)
+ (greet p)
+ (after-greet p)
+ (format #t "> You are socially exhausted. You should rest.~%"))
+
+(around-greet (make <person> #:name "Alice"))
+```
+
+This is just an ad hoc, informally specified, bug ridden version of
+less than half of what CLOS method qualifiers do. There are now four
+methods in the greet API instead of one. Any further specializations
+to `before-greet` and `after-greet` must be sure to call `next-method`
+to emulate the semantics of CLOS before/after qualifiers.
+`around-greet` requires 3 method calls where the CLOS version only
+need to call `call-next-method`. It would be challenging to modify an
+API that people already depended on to accomodate these new wrappers
+without introducing backwards incompatible changes.
+
+`define-method` could be changed in a backwards compatible way to
+support these qualifiers and the method dispatching system could be
+changed to accomodate them.
+
+## Method arguments can only be specialized by class
+
+CLOS supports specializing method arguments on more than just classes.
+Here's an example program that specializes the second argument of
+`greet` on a keyword.
+
+```lisp
+(defclass person ()
+ ((name :initarg :name :accessor name)))
+
+(defmethod greet ((p person) (type (eql :formal)))
+ (format t "Hello, ~a.~&" (name p)))
+
+(defmethod greet ((p person) (type (eql :casual)))
+ (format t "Hey! What's up, ~a?'~&" (name p)))
+
+(defvar p1 (make-instance 'person :name "Alice"))
+
+(greet p1 :formal)
+(greet p1 :casual)
+```
+
+With GOOPS we'd have to do something like this:
+
+```scheme
+(use-modules (ice-9 format) (oop goops))
+
+(define-class <person> ()
+ (name #:init-keyword #:name #:accessor name))
+
+(define-method (greet (p <person>) type)
+ (case type
+ ((formal)
+ (format #t "Hello, ~a.~%" (name p)))
+ ((casual)
+ (format #t "Hey! What's up, ~a?~%" (name p)))))
+
+(define p1 (make <person> #:name "Alice"))
+
+(greet p1 'formal)
+(greet p1 'casual)
+```
+
+This works, but it tightly couples the implementations of each type of
+greeting, and the technique only becomes more cumbersome in real,
+non-trivial programs. What if a child class wanted to specialize just
+the formal greeting? It would require another `case` form with an
+`else` clause that calls `next-method`. It works but it takes extra
+code and it doesn't look as nice, IMO. It would be good to take
+inspiration from
+[https://github.com/sbcl/specializable/](specializable) since it
+allows for user extendable specializers. A similar system could be
+added to GOOPS methods. More advanced method argument specializers
+would be a nice complement to the excellent `(ice-9 match)`
+general-purpose pattern matching module.
+
+## Methods do not support keyword arguments
+
+CLOS methods support keyword arguments *and* rest arguments, GOOPS
+method only support rest arguments. Like rest arguments, keyword
+arguments cannot be specialized. I don't know if this can be added to
+GOOPS in a backwards compatible way or at all. If it's possible, a
+new `define-method*` form, mirroring `define*`, could be added. One
+important difference between CLOS and GOOPS where I like the GOOPS
+behavior better is that generics can support methods of arbitary
+arity, but CLOS generics set the arity and all methods must conform.
+Would this difference complicate a keyword argument implementation?
+
+## Classes, slots, and generics do not have documentation strings
+
+CLOS supports a `:documentation` option in `defclass` as both slot and
+class options, and `defgeneric`, but GOOPS has no equivalent for any
+of them.
+
+Since slots in GOOPS can have arbitrary keyword arguments applied to
+them, a simple `slot-documentation` procedure to get the
+`#:documentation` slot option could be added:
+
+```scheme
+(define (slot-documentation slot)
+ (get-keyword #:documentation (slot-definition-options slot)))
+```
+
+`defgeneric` in CLOS supports docstrings:
+
+```lisp
+(defgeneric greet (obj)
+ (:documentation "Say hello to an object."))
+```
+
+`define-generic` in GOOPS does not:
+
+```scheme
+;; Nothing other than a symbol for the name is allowed.
+(define-generic greet)
+```
+
+The `<generic>` class would need to be modified to accept a
+`#:documentation` initialization argument. `define-generic` syntax
+would need to be modified to optionally accept a documentation string.
+A new `generic-function-documentation` procedure would return the
+documentation for a generic. One complicating factor is that
+`generic-function-name` and `generic-function-methods` are defined in
+libguile, which is C, not Scheme. I don't know if this hypothetical
+`generic-function-documentation` would have to be in C, too.
+
+In CLOS, methods do not accept documentation strings, only generics
+do. The downside of doing the same is that most code using GOOPS that
+I've seen does not use `define-generic` for every generic procedure,
+because `define-method` will create the generic procedure if needed.
+So, it would mean adding a `define-generic` form for every generic
+function that should be documented. Would it make sense to allow
+documentation strings for methods, too? The documentation for a
+generic could then be the union of its own documentation and any of
+its methods that have documentation. There's probably a reason why
+CLOS doesn't do this and it would be best to understand that first.