http://t3x.org/s9fes/s9sos.txt.html

contrib/s9sos.txt

        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