S9SOS - Scheme 9 Simple Object System ------------------------------------- By Nils M Holm, 2010,2012 S9SOS is an extension for object oriented programming in Scheme. It was inspired by CLOS, the Common Lisp Object System, but it is much simpler. However, it still provides: - multiple inheritance - multi-method dispatch - generic procedures - a meta-object protocol Defining Classes ---------------- A "class" is a description of the internal structure of new data objects called the "instances" of that class. It may be thought of as a template for creating data objects sharing the same structure. An instance is sometimes also called an "object", but this name is avoided here, because generic Scheme data objects are also referred to as "objects." An instance is basically a vector whose elements are named. A named element is called a "slot". Programs can retrieve values stored in slots and replace the objects stored in them. See the section on "instances" for details. Slots are either defined in a DEFINE-CLASS form or "inherited" from a superclass. See below for details. (DEFINE-CLASS <NAME> (<CLASS> ...) <SLOT> ...) [SYNTAX] ==> UNSPECIFIC DEFINE-CLASS creates a new class with the given <name> and the given <slot>s. After evaluating DEFINE-CLASS, the new class can be referred to by using <name>. A <slot> may have three different forms: symbol (symbol) (symbol <expression>) In either of the three cases the symbol names the slot. When an <expression> is also given, it will be evaluated by DEFINE-CLASS and stored as a default value for that slot. When no default value is present, () will be used. The new class will be a "subclass" of each <class> listed in the second argument and hence each of those classes will become a "superclass" of the new class. A class is called s subclass of another class, if it inherits any slots or methods (q.v.) from that class. For example: (define-class point () x y) (define-class color () red green blue) (define-class colored-point (point color)) Here the classes POINT and COLOR are "base classes" with no superclasses. POINT defines the instance variables X and Y, and COLOR defines the instance variables RED, GREEN, and BLUE. The COLORED-POINT class is derived from both POINT and COLOR. It does not define any "own" slots ("direct slots"). However, since it is a subclass of both POINT and COLOR, it "inherits" all of their variables, so an instance of COLORED-POINT would have slots named X, Y, RED, GREEN, and BLUE. Methods are inherited in the same way: (define-generic move) (define-method (move (p point) x y) (slot-set! p 'x x) (slot-set! p 'y y)) (move (make-instance colored-point) 17 25) Although the MOVE method is specialized in the POINT class, it also works for the COLORED-POINT class, because COLORED-POINT is derived from POINT. Systems that allow a class to be derived from multiple superclasses is said to support "multiple inheritance." S9SOS support multiple inheritance to some degree. When a naming conflict occurs because multiple superclasses of a given class define slots of the same name, S9SOS will signal an error. (CLASS? OBJECT) ==> BOOLEAN [PROCEDURE] The CLASS? predicate returns truth if the given OBJECT is a class. Meta Object Protocol -------------------- A Meta Object Protocol (MOP) simply exposes the fact that a class is an instance (of the class "class"). Of course, this simple realization has some implications. When you can store values in slots of a class, you can change the way in which the entire object system works. The purpose of a MOP is far beyond the scope of this document. The following procedures are part of the S9SOS MOP. (CLASS-CPL CLASS) ==> LIST [MOP PROCEDURE] Return the Class Precedence List (CPL) of the given class. The CPL of a class specifies the types of generic procedures (q.v.) by which an instance of the given class wants to be handled. However, it does not just name one type, but a list of types in order of descending precedence. When no method specializing in the first (most preferred) type in the list can be found, the second one will be tried, and so on. Given the above definitions of the POINT and COLORED-POINT classes: (map class-name (class-cpl colored-point)) ==> (colored-point point color) The CPL is generated by the COMPUTE-CLASS-CPL procedure. (Q.v.) (CLASS-DIRECT-SLOTS CLASS) ==> LIST [MOP PROCEDURE] Return the "direct slots" of the given class. A direct slot is a slot that has been created when defining the class, i.e. a slot that has *not* been inherited by a superclass: (class-direct-slots point) ==> ((x ()) (y ())) (class-direct-slots colored-point) ==> () (CLASS-DIRECT-SUPERS CLASS) ==> LIST [MOP PROCEDURE] Return the "direct superclasses" of the given class. A direct superclass is a superclass from which the given class was *directly* derived, i.e. the superclass was listed in the definition and not inherited from a direct superclass. In the following example, the class FOO is indirectly inherited by class BAZ, but FOO is not a direct superclass of BAZ: (define-class foo ()) (define-class bar (foo)) (define-class baz (bar)) Hence: (map class-name (class-cpl baz)) ==> (baz bar foo) (map class-name (class-direct-supers baz)) ==> (bar) (CLASS-NAME CLASS) ==> SYMBOL [MOP PROCEDURE] Return the name of the given class. A class itself is an abstract entity that is typically bound to a name. The DEFINE-CLASS form does both of this: create a class of the given name and bind it to the same name: (define-class foo ()) After evaluating the DEFINE-CLASS form, the symbol FOO is bound to a class: foo ==> #<procedure> (The class is encapsulated in a procedure to keep its cyclic structure hidden from the Scheme printer.) Of course, S9SOS classes are first-class objects, so they can be bound to different symbols: (define bar foo) bar ==> #<procedure> The CLASS-NAME procedure always returns the name of the class itself, though (i.e. the name given to it when it was created): (class-name bar) ==> foo (COMPUTE-CLASS-CPL LIST) ==> LIST [MOP PROCEDURE] Given a list of classes, compute the CPL (see CLASS-CPL) of the class listed first in LIST. LIST contains the class whose CPL is to be computed as well as the direct superclasses of that class: (list <root-class> <superclass> ...) COMPUTE-CLASS-CPL returns a new list containing the CPL of <root-class>. The CPL contains all superclasses of the given class as well as the superclasses of the superclasses, and so on. What is particularly important is the order of the CPL, because more specific classes must always precede less specific classes in the list (so that generic procedures (q.v.) will apply the most specific method to each set of arguments.) The CPL is generated by sorting the list of (direct or indirect) subclasses topologically in such a way that classes that are closer to the <root-class> will come first in the resulting list. Given the following class hierarchy (define-class pixel () x y) (define-class voxel (pixel) z) (define-class color () r g b) (define-class c-voxel (voxel color)) the CPL of the C-VOXEL class would look like this: (map class-name (compute-class-cpl (list c-voxel voxel color))) ==> (c-voxel voxel color pixel) When a class inherits from multiple superclasses (like C-VOXEL above), the order of precedence of superclasses at the same "level" of the class hierarchy is determined by the order in which the superclasses were specified when defining the derived class. In the following example, the order of COLOR and VOXEL is reversed: (map class-name (compute-class-cpl (list c-voxel color voxel))) ==> (c-voxel color voxel pixel) (COMPUTE-CLASS-SLOTS LIST) ==> LIST [MOP PROCEDURE] Return the slots of all classes listed in LIST, including the slots inherited by those classes. This procedure is used to generate the slots of a new class. Given the definition of the C-VOXEL class in the previous entry, (compute-class-slots (list voxel color)) ==> ((z ()) (r ()) (g ()) (b ()) (x ()) (y ())) Note that the order of slots does not matter, because slots are (even internally!) referred to by their names. Instances --------- A class is merely a "template" that can be used to generate any number of "instances". An instance of a class is sometimes also called an "object" (although not in this document). (MAKE-INSTANCE CLASS <INIT-ARG> ...) ==> INSTANCE [PROCEDURE] Create a fresh instance of the given class and return it. Each slot of the instance will be initialized with the default value that was assigned to the slot when the class was defined: (define-class point () (x 1) (y 2)) (let ((p (make-instance point))) (list (slot-ref p 'x) (slot-ref p 'y))) ==> (1 2) Slot values can be assigned dynamically when an instance is created by passing some <INIT-ARG>s to the procedure. Each <INIT-ARG> consists of a slot name and a value to be stored in that slot: (let ((p (make-instance point 'x 17 'y 25))) (list (slot-ref p 'x) (slot-ref p 'y))) ==> (17 25) Additional initialization can be performed by a method of the generic procedure INITIALIZE (q.v.) which is applied to the instance after evaluating the <INIT-ARG>s: (define-class s-point () (x 1) (y 2) (scale 100)) (define-method (initialize (p s-point)) (slot-set! p 'x (* (slot-ref p 'scale) (slot-ref p 'x))) (slot-set! p 'y (* (slot-ref p 'scale) (slot-ref p 'y)))) (let ((p (make-instance s-point 'x 17 'y 25))) (list (slot-ref p 'x) (slot-ref p 'y))) ==> (1700 2500) (CLASS-OF OBJECT) ==> CLASS [PROCEDURE] (INSTANCE-CLASS INSTANCE) ==> CLASS [PROCEDURE] Return the class of the given instance: (class-name (class-of (make-instance point))) ==> point The difference between CLASS-OF and INSTANCE-CLASS is that CLASS-OF is more general and will return "built-in" classes (q.v.) for primitive Scheme objects, while INSTANCE-CLASS can only be applied to S9SOS instances. (INITIALIZE INSTANCE) ==> OBJECT [GENERIC] INITIALIZE is a generic procedure providing a method for every class that has been defined using DEFINE-CLASS. Whenever a new instance is created by MAKE-INSTANCE, INITIALIZE is automatically applied to that instance. The methods of INITIALIZE are intended for more elaborate instance initialization. This is done by redefining a method of INITIALIZE. Consider the following example: (define-class timestamp () (time (sys:time))) In this example, the TIME slot would always be initialized with the value that SYS:TIME delivered when the TIMESTAMP *class* was created. To initialize the TIME slot with the current time when an instance of TIMESTAMP is created, a method specializing in the TIMESTAMP class would be added to INITIALIZE: (define-method (initialize (t timestamp)) (slot-set! t 'time (sys:time))) (INSTANCE? OBJECT) ==> BOOLEAN [PROCEDURE] The INSTANCE? predicate returns truth if the given OBJECT is an instance of any class. Note that primitive Scheme objects are *not* instances of built-in classes, so (instance? "xyz"), for example, will deliver #F. (SLOT-REF INSTANCE SYMBOL) ==> OBJECT [PROCEDURE] Return the value stored in the slot named SYMBOL of the given INSTANCE. (SLOT-SET! INSTANCE SYMBOL OBJECT) ==> UNSPECIFIC [PROCEDURE] Replace the value stored in the slot named SYMBOL of the given INSTANCE with the given OBJECT. Generic Procedures ------------------ A "generic procedure" is a set of procedures that specialize in handling different data types. A procedure that is contained in a generic procedure is also called a "method". Generic procedures select methods by "multi-method dispatch". This means that *all* arguments of a generic procedure are taken into consideration when looking for the most specific methods for handling a given application. Generic procedures are a rather heavy-weight tool. Using it for trivial tasks is highly inefficient. (DEFINE-GENERIC <NAME>) ==> UNSPECIFIC [SYNTAX] Define a new generic procedure and bind it to the given <name>. When a generic procedure with the given name already exists, all methods of that generic will be removed! (DEFINE-METHOD (<NAME> <ARGUMENT> ...) <BODY>) [SYNTAX] ==> UNSPECIFIC (DEFINE-METHOD (<NAME> <ARGUMENT> ... . SYMBOL) <BODY>) ==> UNSPECIFIC Add a method to the generic procedure that is bound to <name>. A method is like an ordinary procedure, but it is never applied directly but always dispatched by the corresponding generic procedure. (See APPLY-GENERIC for details on generic procedure application). Typically the arguments of a generic procedure are of the form (symbol class) where the symbol names the argument and the class is used to specialize the method. When only a symbol is given, the class defaults to <TYPE>, which matches any type (i.e. the method does not specialize at all). A built-in class may be specified to specialize a method in a native Scheme data type: (define-generic conc) (define-method (conc (a <pair>) b) (append a b)) (define-method (conc (a <string>) (b <string>)) (string-append a b)) (define-method (conc (a <char>) (b <string>)) (string-append (string a) b)) (conc '(1 2 3) 4) ==> (1 2 3 . 4) (conc "abc" "def") ==> "abcdef" (conc #\h "ello") ==> "hello" Like a procedure a method may have a "rest" argument that is placed after the dot of an improper argument list. This argument binds to a list of remaining arguments after binding the fixed variables of the method. The rest argument does not play any role when dispatching a method. (APPLY-GENERIC GENERIC LIST) ==> OBJECT [PROCEDURE] Apply a generic procedure to a list of arguments. The length of LIST much match the number of arguments of GENERIC. A generic procedure is applied to its arguments by first selecting all methods that are "applicable" to the given arguments. This is done by the COMPUTE-APPLICABLE-METHODS procedure (q.v.). Once the set of applicable methods has been obtained, the most specific one of the methods that still matches the given arguments will be select. This method is called the "effective method". It is computed by the COMPUTE-EFFECTIVE-METHOD (q.v.) procedure. Finally the effective method is applied to the given arguments and the result is returned. This final step is the same as in the APPLY procedure of Scheme. In case the set of applicable methods is empty, APPLY-GENERIC calls the NO-APPLICABLE-METHOD procedure. (CALL-NEXT-METHOD) ==> OBJECT [PROCEDURE] The CALL-NEXT-METHOD procedure is bound internally in every method. It pass the arguments received by that message on to the next-unspecific method. See COMPUTE-EFFECTIVE-METHOD for details. Here is an example: (define-class foo ()) (define-class bar (foo)) (define-generic frob) (define-method (frob (x foo)) '(foo)) (define-method (frob (x bar)) (cons 'bar (call-next-method))) (frob (make-instance foo)) ==> (foo) (frob (make-instance bar)) ==> (bar foo) When CALL-NEXT-METHOD is called and there is no less specific method, NO-NEXT-METHOD (q.v.) is called instead. (COMPUTE-APPLICABLE-METHODS LIST1 LIST2) [MOP PROCEDURE] ==> LIST LIST1 is a list of methods and LIST2 is an (already evaluated) argument list to be passed to one (or multiple) of of these methods. A method is "applicable" to a set of arguments, if the class of each member of its "specializer" matches the class of the corresponding argument. A specializer is the signature of a method. It is a list of classes that was specified (express or implied) in the DEFINE-METHOD form that defined the method. For example, the specializer of the method (define-method (foo (a <bar>) (b <baz>)) (frob a b)) would be (<bar> <baz>). An argument matches a class <bar>, if it is of the same class or of a more specific class than <bar>. A class <x> is more specific than a class <y>, if <x> was derived (directly or indirectly) from <x>. For example: (define-class point () x y) (define-class c-point (point) color) (define-generic move) (define-method (move (p point) (x <number>) (y <number>)) (slot-set! p 'x x) (slot-set! p 'y y)) (define-generic set-color) (define-method (set-color (cp c-point) color) (slot-set! p 'color color)) In this case, the class C-POINT is more specific than POINT (a C-POINT is a POINT with a color), so the method specializing in POINT can also be used to move a C-POINT. The SET-COLOR method, however, specializes in C-POINT and POINT is *less* specific than C-POINT, so SET-COLOR cannot be used to change the color of a POINT. Hence: (compute-applicable-methods (generic-methods move) (list (make-instance c-point) 1 2)) ==> (#<method>) But: (compute-applicable-methods (generic-methods set-color) (list (make-instance point) 'yellow)) ==> () Sometimes multiple methods of the same generic procedure may be applicable to a given set of arguments. In this case, the COMPUTE-APPLICABLE-METHODS procedure returns all applicable methods in no specific order. (COMPUTE-EFFECTIVE-METHOD LIST1 LIST2) [MOP PROCEDURE] ==> PROCEDURE LIST1 is a set of applicable methods (see COMPUTE-APPLICABLE- METHODS) and LIST2 is an (already evaluated) list of arguments to be passed to one of those methods. The "effective method" of a set of methods that the most specific method (the one with the most specific classes in its specializer) that still matches the classes of the arguments in LIST2. Consider the following generic: (define-class foo ()) (define-class bar (foo)) (define-generic frob) (define-method (frob (x <type>) 0) (define-method (frob (x bar)) 1) BAR is more specific than FOO, so the FROB method specializing in BAR will be selected when LIST2 consists of an instance of the class BAR. However BAR is too specific to handle instances of the class FOO, so the method specializing in <TYPE> will be selected when LIST2 consists of an instance of FOO. COMPUTE-EFFECTIVE-METHOD finally creates a procedure that applies arguments of the given types to the selected method. The resulting procedure binds the symbol CALL-NEXT-METHOD internally to a procedure that passes the received arguments to the next-unspecific method. See CALL-NEXT-METHOD for details. COMPUTE-EFFECTIVE-METHOD returns the procedure described above. (GENERIC-METHODS GENERIC) ==> LIST [MOP PROCEDURE] Return the list of methods of the given generic procedure. Note that methods are not ordinary procedures. In S9SOS, they are represented by lists of the form (specializer procedure) where SPECIALIZER is the signature of the method and PROCEDURE an ordinary procedure handling arguments matching the signature. So to find out what kind of arguments a generic procedure can handle, you may apply the following procedure to it: (define (signatures generic) (map (lambda (sig) (map class-name sig)) (map car (generic-methods generic)))) (NO-APPLICABLE-METHOD) ==> UNDEFINED [MOP PROCEDURE] When a generic procedure is applied to a type for which it does not have a specialized method, NO-APPLICABLE-METHOD is called. By default this procedure signals an error, but it can be redefined to handle the "no applicable method" condition differently: (define-generic frob) (frob 17) ==> error ; no applicable method (define (no-applicable-method) 'default-value) (frob 17) ==> default-value Note, though, that redefining NO-APPLICABLE-METHOD will affect *all* generic procedures, so this should be planned carefully. (NO-NEXT-METHOD) ==> UNDEFINED [MOP PROCEDURE] When a method call CALL-NEXT-METHOD and there is no less specific method (see COMPUTE-EFFECTIVE-METHOD), then the NO-NEXT-METHOD procedure is invoked. By default it will signal an error: (define-class foo (<type>)) (define-generic frob) (define-method (frob x) (cons '<type> (call-next-method))) (define-method (frob (x foo)) (cons 'foo (call-next-method))) (frob (make-instance foo)) ==> error ; no next method However, NO-NEXT-METHOD can be redefined to handle the "no next method" condition differently: (define (no-next-method) '(top-of-hierarchy)) (frob (make-instance foo)) ==> (foo <type> top-of-hierarchy) Note, though, that redefining NO-NEXT-METHOD will affect *all* generic procedures, so this should be planned carefully. Dynamic MOP Procedures ---------------------- (ADD-METHOD GENERIC LIST PROCEDURE) [MOP PROCEDURE] ==> UNSPECIFIC ADD-METHOD is a low-level interface for adding a new method to a generic procedure. GENERIC is the generic procedure to which the method will be added, LIST is a specializer (see COMPUTE- APPLICABLE-METHODS), and PROCEDURE is a procedure handling the types in the specializer. The first argument of PROCEDURE must be named CALL-NEXT-METHOD, because it will be used to bind the next-unspecific method of the generic (see COMPUTE-EFFECTIVE-METHOD). The remaining arguments must match the specializer. ADD-METHOD performs little or no sanity checking. Caveat utilitor! (MAKE-CLASS SYMBOL LIST <SLOT> ...) ==> CLASS [MOP PROCEDURE] MAKE-CLASS is a low-level interface for creating a new class. SYMBOL will name the new class, LIST is a list of superclasses, and each slot defines a slot name and its default value. See DEFINE-CLASS for details. MAKE-CLASS does *not* add a method to INITIALIZE, so the following example will result in (NO-APPLICABLE-METHOD): (make-instance (make-class 'foo '())) To create a class dynamically, use (let ((foo (make-class 'foo '()))) (add-method initialize (list foo) (lambda args #t)) (make-instance foo)) (MAKE-GENERIC) ==> GENERIC [MOP PROCEDURE] MAKE-GENERIC is a low-level interface for creating a new generic procedure with no methods. Class Hierarchy --------------- The basic S9SOS class hierarchy consists of the following classes: <TYPE> <INSTANCE> <CLASS> <GENERIC> <BUILT-IN> <TYPE> is the top of the entire class hierarchy, hence a method specializing in <TYPE> will match any kind of argument. <INSTANCE> and <BUILT-IN> are the bases of two disjoint sets of types, where <INSTANCE> forms the base of the "OOP part" of S9SOS and <BUILT-IN> the base of the native Scheme type hierarchy. Note in particular that this means that a <BUILT-IN> is *not an <INSTANCE>, so (make-instance <integer>) will *not* create an instance of <INTEGER>. <CLASS> and <GENERIC> are the only instance classes that are necessary to make S9SOS work. This small hierarchy reflects nicely that both classes and generics are in fact ordinary instances. Just try (class-name (class-of <class>)) or (slot-ref initialize 'methods) The sub-hierarchy of <BUILT-IN> lists the native Scheme data types. As noted above, these class are abstract and cannot be used to form valid instances. Their only purpose is to allow method to specialize in Scheme data types. <BUILT-IN>, the top of the hierarchy matches any Scheme type. <BUILT-IN> <BOOLEAN> <CHAR> <EOF-OBJECT> <NULL> <NUMBER> <INTEGER> <PAIR> <PORT> <INPUT-PORT> <OUTPUT-PORT> <PROCEDURE> <ARRAY> <STRING> <VECTOR> <SYMBOL> The <BUILT-IN> tree contains some subclasses that do not have direct counterparts in the set of Scheme types, like <NUMBER> or <ARRAY>. Built-in types are hard-wired into the CLASS-OF procedure: (class-of '(a b c)) ==> <pair> (class-of '#(1 2 3)) ==> <vector> (class-of '()) ==> <null> So these types can be used for specializing methods, but no new subclasses of <BUILT-IN> can be defined at user-level, because they would not be covered by CLASS-OF.
contact | privacy