OOP with GNU Guile and GOOPS (2014/03/02)
Having worked with the Ruby programming language for many years I started to get interested in Scheme. The Scheme programming language has a small and powerful core supporting closures, hygienic macros, first-class continuations, and of course the famous interpreter functions eval and apply.
However one thing I don’t like about Scheme is that there are different function names for each type of arguments. E.g. adding numbers is done with +, adding lists is done with append, and adding strings is done with string-append.
(+ 2 3) ; 5 (append '(1) '(2 3)) ; (1 2 3) (string-append "a" "bc") ; "abc"
The same program would be much less verbose if + was extended to work with strings and lists, too:
(+ 2 3) ; 5 (+ '(1) '(2 3)) ; (1 2 3) (+ "a" "bc") ; "abc"
GOOPS: The Guile extension for object-oriented programming
It turns out that GNU Guile (the Scheme interpreter of the GNU project) has a mature extension which facilitates this. GOOPS is inspired by the Common Lisp Object System (CLOS). GOOPS not only provides polymorphic methods, it even lets you replace existing functions with polymorphic ones:
(use-modules (oop goops)) (define-method (+ (a <list>) (b <list>)) (append a b)) (define-method (+ (a <string>) (b <string>)) (string-append a b)) (+ 2 3) ; 5 (+ '(1) '(2 3)) ; (1 2 3) (+ "a" "bc") ; "abc"
The define-class method is the normal way to define a class. GOOPS requires you to define the instance attributes when defining the class. The following example defines a class \<a\> with attribute x and a + method. The make method is used to create the instance a of the class \<a\>.
(use-modules (oop goops)) (define-class <a> () (x #:init-value 0 #:init-keyword #:x #:getter get-x)) (define-method (+ (u <a>) (v <a>)) (make <a> #:x (+ (get-x u) (get-x v)))) <a> ; #<<class> <a> 2103c30> (define a (make <a> #:x 123)) a ; #<<a> 22a2440> (get-x a) ; 123 (+ a a) ; #<<a> 23713e0> (get-x (+ a a)) ; 246
Constructors and the next method
One can define a shorthand for instantiating objects. E.g. one can define a method which sets the attribute #:x to the argument multiplied by two.
(use-modules (oop goops)) (define-class <a> () (x #:init-value 0 #:init-keyword #:x #:getter get-x)) (define (make-a x2) (make <a> #:x (* 2 x2))) (define a2 (make-a 123)) ; #<<a> 1689740> (get-x a2) ; 246
IMHO it is better though to define a modified constructor directly. This is more involved but also possible.
(use-modules (oop goops) (ice-9 optargs)) (define-class <a> () (x #:init-value 0 #:init-keyword #:x #:getter get-x)) (define-method (initialize (self <a>) initargs) (let-keywords initargs #f (x2) (next-method self (list #:x (* 2 x2))))) (define a3 (make <a> #:x2 123)) (get-x a3) ; 246
Note the call to next-method. This essentially calls the next less specialized method for that generic function. Here is another example using an inheriting class \<b\> to illustrate the concept.
(use-modules (oop goops)) (define-class <a> ()) (define-class <b> (<a>)) (define-method (test (self <a>)) "a") (define-method (test (self <b>)) (string-append (next-method) "b")) (test (make <a>)) ; "a" (test (make <b>)) ; "ab"
Note that GOOPS does not implicitly create a metaclass. The following example shows how to define a metaclass \<m\<c\>\> with a class method.
(use-modules (oop goops)) (define-class <m<c>> (<class>)) (define-method (test <m<c>>) "m") (define-class <c> () (x #:init-keyword #:x #:getter get-x) #:metaclass <m<c>>) (define o (make <c> #:x 5)) o ; #<<c> 2825d40> (test (class-of o)) ; "m"
One can also use GOOPS to change the way how objects get displayed and what the REPL writes to the terminal. E.g. one can define the method write for the class \<a\> to change the way the Guile REPL displays objects of that class.
(use-modules (oop goops)) (define-class <a> () (x #:init-value 0 #:init-keyword #:x #:getter get-x)) (define a (make <a> #:x 5)) a ; #<<a> 2c64140> (define-method (write (self <a>) port) (format port "(make <a> #:x ~a)" (get-x self))) a ; (make <a> #:x 5)
Furthermore one can implement the method display to define the way objects get displayed in formatted output.
(define-method (display (self <a>) port) (format port "*~a*" (get-x self))) (format #t "~a~&" a) ; *5*
I have now used GOOPS for a little while. Coming from Ruby there are a few gotchas when using GOOPS and Guile’s module system. For example one needs to use a #:re-export statement when using a module to replace the core binding for the + operator.
All in all GOOPS makes quite a mature impression and I think it makes Scheme much more amenable to developers who are used to thinking in terms of objects and classes.
To quote Guile’s foreign function interface documentation:
The more one hacks in Scheme, the more one realizes that there are actually two computational worlds: one which is warm and alive, that land of parentheses, and one cold and dead, the land of C and its ilk.
Any comments and suggestions are welcome
If necessary it is also possible to create objects, classes, and metaclasses dynamically. The following example instantiates the object v of class \<v\> of metaclass \<m\<v\>\>. Furthermore the generic test is implemented for \<v\>.
(use-modules (oop goops)) (define-generic test) (define <m<v>> (make <class> #:dsupers `(,<class>) #:slots '() #:name "<m<v>>")) (define <v> (make <m<v>> #:dsupers `(,<object>) #:slots '() #:name "<v>")) (add-method! test (make <method> #:specializers `(,<v>) #:procedure (lambda (self) "v"))) (define v (make <v>)) v ; #<<v> 2d5a750> (test v) ; "v"
I have reduced the use of slot-ref during a discussion on the GUILE user list.
- GNU Guile
- Structure and Interpretation of Computer Programs
- The Little Schemer
- The Seasoned Schemer
- The Reasoned Schemer
- COOPS object system for Chicken Scheme
- Clojure multi-methods