summaryrefslogtreecommitdiff
path: root/posts/2022-10-05-goops-issues.md
blob: d4eab3e77a62a4f040f02f6ea99ce0b5335abc06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
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"))
```

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`.

```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.

## 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:

```lisp
(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.

```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 [SBCL's specializable
library](https://github.com/sbcl/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.
`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.