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. 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:
(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:
(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:
(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"))
Expected output:
> You take a deep breath.
> You gather the courage to talk to Alice.
Hello, Alice!
> That wasn't so bad.
> You are socially exhausted. You should rest.
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
.
(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.
No control over method combination algorithm
This is related to the qualifier issue. Those before/after/around qualifiers are part of the standard CLOS method combination algorithm. There are other ways to combine methods in CLOS and generics can be configured to use a non-standard one:
(defgeneric multi-greet (p)
(:method-combination progn))
The multi-greet
generic would call all of the primary methods
associated with it, not just the most specific one. I haven't seen a
way to control how methods are applied in GOOPS. Adding this feature
would be complicated by the fact that generics do not have a fixed
arity, but I think it would still be possible to implement. Maybe for
the GOOPS version of the progn
combination, GOOPS would call all of
the primary methods that match the number of arguments passed to the
generic and ignore the others.
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.
(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:
(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 SBCL's specializable
library 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:
(define (slot-documentation slot)
(get-keyword #:documentation (slot-definition-options slot)))
defgeneric
in CLOS supports docstrings:
(defgeneric greet (obj)
(:documentation "Say hello to an object."))
define-generic
in GOOPS does not:
;; 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.
defmethod
also accepts documentation strings, but Guile’s
define-method
does not.
Generics cannot be merged outside of a module
If I'm writing a Guile script, not a library module, I use
use-modules
to import the modules I need to get the job done.
However, if two or more modules export a generic with the same name,
there's no way to tell use-modules
that I'd like them to be merged
into a single generic. Merging generics is an option when using the
define-module
form, but it doesn't feel right to use it when you're
not actually writing a reusable module and it feels weird to have to
change the syntax of how I'm importing modules just because generics
are present.