Chapter 7. Types and Objects

Table of Contents

Type System
Core Procedures and Predicates
Derived Procedures and Predicates
Hooks
Standard Types
Generic Procedures
Defining Generic Procedures
Defining Methods
Invoking Generic Procedures
Procedures on Methods
Miscellaneous
Object System
Classes
Slots
Instantiation
Inheritance

Type System

Requires: (import type-system)

SISC's extensible type system provides programmatic access to the type information of values and provides a core set of type testing and comparison procedures. The type system is extensible in two ways. Firstly any new native types are recognised automatically. Secondly, hooks are provided for Scheme-level extensions of the type-system.

By convention, type names start with < and end with >, with normal Scheme identifier naming conventions applying for everything in-between, i.e. all lower-case with words separated by dashes. For example, <foo-bar-baz>. This convention helps to visually distinguish type names from names of procedures and top-level data bindings.

Core Procedures and Predicates

procedure: (type-of value) => type

Returns the type of value. There is no standard representation for types, leaving type extensions free to choose a representation that suits them most.

The procedure is equipped with an extension hook, type-of-hook. See the section called “Hooks” for more details on hooks. The default implementation of the hook type-of returns a type based on the Java type of the internal representation of value.

(type-of 1)  ;=> #<scheme sisc.data.Quantity>
(type-of (lambda (x) x))  ;=> #<scheme sisc.data.Closure>
              

procedure: (type<= type1 type2) => #t/#f

Returns #t if type1 is a sub-type of type2.

The predicate is equipped with an extension hook, type<=-hook. See the section called “Hooks” for more details on hooks. The default implementation of the hook determines sub-typing based on the inheritance relationship of the Java types representing native types.

(type<= (type-of 'a) (type-of 'b))  ;=> #t
(type<= (type-of 'b) (type-of 'a))  ;=> #t
(type<= (type-of 1) (type-of 'a))  ;=> #f
(type<= (type-of 1) <number>)  ;=> #t
(type<= (type-of 'a) <symbol>)  ;=> #t
(type<= <number;> <symbol>)  ;=> #f
(type<= <symbol;> <number>)  ;=> #f
(type<= <number> <value>)  ;=> #t
(type<= <symbol> <value>)  ;=> #t
              

procedure: (compare-types type1 type2 type3) => 'equal,'more-specific,'less-specific

Determines the relationship of type1 and type2 with respect to type3. type3 must be a sub-type of type1 and type2. type1 and type2 are first compared using type<=. If that comparison indicates that the types are disjoint (i.e. type1 is not sub-type of type2, type2 is not a sub-type of type1 and the types are not equal) then additional information from type3 is taken into account for the comparison.

The predicate is equipped with an extension hook, compare-types-hook that is invoked in the case the comparison of type1 with type2 using type<= finds the two types to be disjoint. See the section called “Hooks” for more details on hooks. The default implementation of the hook returns an error

(compare-types <number> <value> <number>)
  ;=> 'more-specific
(compare-types <value> <number> <number>)
  ;=> 'less-specific
(compare-types <number> <number> <number>)
  ;=> 'equal
              

Derived Procedures and Predicates

The type system's derived procedures and predicates are implemented in terms of the core procedures and predicates.

procedure: (instance-of? value type) => #t/#f

Determines whether value is an instance of type.

The predicate obtains value's type using type-of and then compares it to type using type<=.

(instance-of? 1 <number>)  ;=> #t
(instance-of? 'a <symbol>)  ;=> #t
(instance-of? 1 <symbol>)  ;=> #f
(instance-of? 1 <value>)  ;=> #t
              

procedure: (type= type1 type2) => #t/#f

Determines whether two types are equal by comparing them using type<=.

(type= (type-of 'a) (type-of 'b))  ;=> #t
(type= (type-of 1) (type-of 'a))  ;=> #f
(type= (type-of 1) <number>)  ;=> #t
(type= (type-of 'a) <symbol>)  ;=> #t
(type= <number;> <symbol>)  ;=> #f
              

procedure: (types<= type-list1 type-list2) => #t/#f

Determines whether all of the types in type-list1 are sub-types of the the corresponding (by position) types in type-list2.

A pair-wise comparison of the elements in the two lists using type<= is performed until a test returns #f, in which case #f is returned, or one (or both) of the lists has been exhausted, in which case #t is returned.

(types<= (list <number> <symbol>)
         (list <value> <value>))  ;=> #t
(types<= (list <number> <symbol>)
         (list <number> <number>))  ;=> #f
              

procedure: (instances-of? value-list type-list) => #t/#f

Determines whether all of the values in value-list are instances of the the corresponding (by position) types in type-list.

A pair-wise comparison of the elements in the two lists using instance-of? is performed until a test returns #f, in which case #f is returned, or one (or both) of the lists has been exhausted, in which case #t is returned.

(instances-of? (list 1 'a)
               (list <number> <symbol>))  ;=> #t
(instances-of? (list 1 'a)
               (list <number> <number>))  ;=> #f
              

procedure: (types= type-list1 type-list2) => #t/#f

Determines whether all of the types in type-list1 are equal to the the corresponding (by position) types in type-list2.

A pair-wise comparison of the elements in the two lists using type= is performed until a difference is found, in which ase #f is returned, or one (or both) of the lists has been exhausted, in which case #t is returned.

(types= (list <number> <symbol>)
        (list <number> <symbol>))  ;=> #t
(types= (list <number> <symbol>)
        (list <number> <number>))  ;=> #f
              

Hooks

Hooks are the main mechanism by which Scheme code can extend the default type system. The core type system procedures type-of, type<= and compare-types all provide such hooks, called type-of-hook, type-<=-hook, and compare-types-hook respectively.

Extension takes place by installing labelled handler procedures on the hook, which is done by invoking the hook procedure. Installing a handler procedure with a label of an already installed procedure replaces the latter with the former.

The handler procedures are called with a next procedure as the first argument and all the arguments of the call to the hook-providing procedures as the remaining arguments. Typically a handler procedure first determines whether it is applicable, i.e. is capable of performing the requested comparison etc. If not it calls the next handler procedure, which invokes the next hook or, if no further hooks exist, the default implementation of the hooked procedure.

Example 7.1. Hook Installation

This example shows how SISC's record type module adds record types to the type system by installing handler procedure on type-of-hook and type<=-hook.

(type-of-hook 'record
  (lambda (next o)
    (if (record? o)
        (record-type o)
        (next o))))

(type<=-hook 'record
  (lambda (next x y)
    (cond [(record-type? x)
           (if (record-type? y)
               (eq? x y)
               (type<= <record> y))]
          [(record-type? y) #f]
          [else (next x y)])))
        

Standard Types

The type system pre-defines bindings for the native types corresponding to all the data types defined in R5RS: <eof>, <symbol>, <list>, <procedure>, <number>, <boolean>, <char>, <string>, <vector>, <input-port>, <output-port>. One notable exception is that pairs and null are combined into a <list> type.

The type system also defines a <value> type that is the base type of all SISC values, i.e. all SISC values are instances of <value> and all types are sub-types of <value>.

The representations of other native types can be obtained using

procedure: (make-type symbol) => type

Constructs a type representing a built-in type. The symbol must denote a Java class that is a sub-class of sisc.data.Value, the base of the SISC value type hierarchy.

(define <record> (make-type '|sisc.modules.record.Record|))
(type<= <record> <value>)  ;=> #t
              

Generic Procedures

Requires: (import generic-procedures)

Generic procedures are procedures that select and execute methods based on the types of the arguments. Methods have a type signature, which generic procedures use for method selection, and contain a procedure which is invoked by generic procedures when the method has been selected for execution.

Generic procedures have several advantages over ordinary procedures:

  • It is not necessary to come up with unique names for procedures that perform the same operation on different types of objects. This avoids cluttering the name space. All these procedures can be defined separately but yet be part of the same, single generic procedure.

  • The functionality of a generic procedure can be extended incrementally through code located in different places. This avoids "spaghetti code" where adding a new type of objects requires changes to existing pieces of code in several locations.

  • Code using generic procedures has a high degree of polymorphism without having to resort to ugly and hard-to-maintain test-type-and-dispatch branching.

Generic procedures make extensive use of SISCs type system. See the section called “Type System”.

The use of generic procedures proceeds through three stages:

  1. definition of the generic procedure

  2. adding of methods to the generic procedure

  3. adding of methods to the generic procedure

The adding of methods can be interleaved with invocation, i.e. methods can be added to generic procedures while they are in use.

Defining Generic Procedures

There are one procedure and two special forms for defining generic procedures. Typical usage will employ one of the special forms.

procedure: (make-generic-procedure generic-procedure ...) => generic-procedure

Creates a generic procedure. If generic-procedure parameters are specified, then their method lists are merged, in effect combining the generic procedures into one. For more details on generic procedure combination see the section called “Generic Procedure Combination”.

(define pretty-print1 (make-generic-procedure))
(define pretty-print2 (make-generic-procedure))
;=> <procedure>
(define pretty-print (make-generic-procedure pretty-print1
                                             pretty-print2))
              

syntax: (define-generic name generic-procedure ...)

Creates a binding for name to a new generic procedure.

This form is equivalent to (define name (make-generic-procedure generic-procedure ...)).

(define-generic pretty-print1)
(define-generic pretty-print2)
(define-generic pretty-print pretty-print1 pretty-print2)
              

syntax: (define-generics form ...)

where form is of the form name or (name generic-procedure ...)

Creates bindings for several new generic procedures.

The form expands into several define-generic forms.

(define-generic pretty-print1)
(define-generic pretty-print2)
(define-generics
  foo
  (pretty-print pretty-print1 pretty-print2)
  bar)
              

Defining Methods

Methods can be define and subsequently added to generic procedures, or the two operations can be combined, which is the typical usage.

There is one procedure and one special form to create methods:

procedure: (make-method procedure type-list rest?) => method

Creates a new method containing procedure whose type signature is type-list. If rest? is #t then the procedure can take rest arguments.

Generic procedures always invoke method procedures with a special next: argument as the first parameter (see the section called “Method Selection”), followed by all the arguments of the generic procedure invocation. Hence procedure needs to accept (length type-list)+1 arguments.

(make-method (lambda (next x y) (next x y))
             (list <number> <number>)
             #f)
  ;=> <method>
(make-method (lambda (next x . rest) (apply + x rest))
             (list <number>)
             #t)
  ;=> <method>
              

syntax: (method signature . body) => method

where signature is of the form ([(next: next)] (type param) ... [ . rest])
and body can contain anything that is valid inside the body of a lambda.

Creates a method.

This form is similar to a lambda form, except that all parameters must be typed. The form expands into an invocation of the make-method procedure.

The first parameter name in the method's signature can be the special next: parameter. See the section called “Method Selection”.

(method ((next: next)(<number> x)(<number> y)) (next x y))
  ;=> <method>
(method ((<number> x) . rest) (apply + x rest))
  ;=> <method>
              

There are two procedures to add methods to a generic procedure:

procedure: (add-method generic-procedure method)

Adds method to generic-procedure. Any existing method with the same signature as method is removed.

Method addition is thread-safe.

(define-generic m)
(add-method m (method ((next: next)(<number> x)(<number> y)) (next x y)))
(add-method m (method ((<number> x) . rest) (apply + x rest)))
              

procedure: (add-methods generic-procedure method-list)

Adds all methods in method-list to generic-procedure. Any existing method with the same signature as one of the methods in method-list is are removed. When several methods in method-list have the same signature, only the last of these methods is added.

Method addition is thread-safe. Calling add-methods instead of add-method when adding several methods to a generic procedure is more efficient.

(define-generic m)
(add-methods m (list (method ((next: next)(<number> x)(<number> y)) (next x y))
                     (method ((<number> x) . rest) (apply + x rest))))
              

The creation of methods and adding them to generic procedures can be combined using one of two special forms:

syntax: (define-method (generic-procedure . signature) . body)

Creates a method and adds it to generic-procedure.

This form is equivalent to (add-method generic-procedure (method signature . body)

(define-generic m)
(define-method (m (next: next)(<number> x)(<number> y)) (next x y)))
(define-method (m (<number> x) . rest) (apply + x rest))
              

syntax: (define-methods generic-procedure (signature . body) ...)

Creates several methods and adds them to generic-procedure.

This form is equivalent to (add-methods generic-procedure (list (method signature . body) ...)

(define-generic m)
(define-methods m
  [((next: next)(<number> x)(<number> y)) (next x y)]
  [((<number> x) . rest) (apply + x rest)])
              

The list of methods contained in a generic procedure can be obtained as follows:

procedure: (generic-procedure-methods generic-procedure) => method-list

Returns the list of methods currently associated with generic-procedure.

(define-generic m)
(define-methods m
  [((next: next)(<number> x)(<number> y)) (next x y)]
  [((<number> x) . rest) (apply + x rest)])
(generic-procedure-methods m)  ;=> (<method> <method>)
              

Invoking Generic Procedures

Generic procedures are invoked like ordinary procedures. Upon invocation, generic procedures compute a list of applicable methods, ordered by their specificity, based on the types of the parameters supplied in the invocation. If the resulting list is empty an error is raised. Otherwise the first (i.e. most specific) method is invoked. The remaining methods come into play when a method invokes the "next best matching method". See the section called “Method Selection” for details on the method selection algorithms.

The logic by which generic procedures select methods for invocation is made accessible to the programmer through the following procedures:

procedure: (applicable-methods generic-procedure type-list) => method-list

Returns all methods of generic-procedure that are applicable, as determined by method-applicable? to parameters of the types specified in type-list. The methods are returned ordered by their specificity, determined by pair-wise comparison using compare-methods.

(define-generic m)
(define-methods m
  [((next: next)(<number> x)(<number> y)) (next x y)]
  [((<number> x) . rest) (apply + x rest)])
(applicable-methods m (list <number> <number>))
  ;=> (<method> <method>)
              

procedure: (method-applicable? method type-list) => #t/#f

Determines whether method is applicable to arguments of the types specified in type-list.

The rules for determining method applicability are defined in the section called “Method Selection”.

(method-applicable? (method ((next: next)(<number> x)(<number> y)) (next x y))
                    (list <number>))
  ;=> #f
(method-applicable? (method ((<number> x) . rest) (apply + x rest))
                    (list <number>))
  ;=> #t
              

procedure: (compare-methods method method type-list) => 'equal,'more-specific,'less-specific

Determines the relationship of two methods by comparing their type signatures against each other and using the supplied type-list for disambiguation. Both methods must be applicable to type-list, as determined by method-applicable?.

The comparison algorithm is described in the section called “Method Selection”.

(compare-methods (method ((next: next)(<number> x)(<number> y)) (next x y))
                 (method ((<number> x) . rest) (apply + x rest))
                 (list <number> <number>))
                ;=> 'more-specific
              

Calling a generic will dispatch on the argument types as described above. This dispatch can change if new methods are added to a generic procedure. On occasion the programmer may wish to fix the dispatch of a particular generic function, either to guarantee a specific function is called for a given part of a Scheme program, or to improve performance by avoiding the type dispatch at each call. SISC provides a syntactic form which allows the programmer to bind/rebind a generic procedure to a new lexical variable which is the monomorphized variant of the function call.

syntax: (let-monomorphic bindings expressions ...)

where bindings are of the form ((generic type...) ...)
or (((binding generic) type...) ...)

In the former binding form, the generic procedure specified by generic is rebound lexically with the same name, and monomorphized to the method which is applicable for the given types. In the latter form, the generic is rebound lexically to the new name specified by binding. Both forms may be used in a given call to let-monomorphic.

The bindings are made as if by let, i.e. no assumptions can be made as to the order in which they are bound. The expressions are evaluated as in let as well, in order using an implicit begin.

(let-monomorphic ([foo-generic <number> <string>]
                  [(bar bar-generic) <char>])
  (foo-generic 3 "four")
  (bar #\x))
              

Procedures on Methods

Methods are instances of the abstract data type <method>, which has range of procedures:

procedure: (method? value) => #t/#f

Returns #t if value is a method, #f otherwise.

(method? (method ((<number> x) . rest) (apply + x rest)))
  ;=> #t
(method? (lambda (x) x))
  ;=> #f
              

procedure: (method-procedure method) => procedure

Returns method's body as a procedure. Note that a method's procedure always takes a "next method" procedure as the first argument. See the section called “Method Selection”.

((method-procedure (method ((<number> x) . rest) (apply + x rest)))
 #f 1 2 3)
  ;=> 6
              

procedure: (method-types method) => type-list

Returns method's type signature, i.e. the types of the declared mandatory parameters.

(method-types (method ((next: next)(<number> x)(<number> y) . rest) (next x y)))
  ;=> (<number> <number>)
              

procedure: (method-rest? method) => #t/#f

Returns #t if method has a rest parameter, #f otherwise.

(method-rest? (method ((<number> x) . rest) (apply + x rest)))
  ;=> #t
(method-rest? (method ((next: next)(<number> x)(<number> y) . rest) (next x y)))
  ;=> #f
              

procedure: (method-arity method) => number

Returns the number of mandatory arguments of method. Note that the special next: parameter is not counted.

(method-arity (method ((<number> x) . rest) (apply + x rest)))
  ;=> 1
(method-arity (method ((next: next)(<number> x)(<number> y) . rest) (next x y)))
  ;=> 2
              

procedure: (method= method method) => #t/#f

Returns #t if the two methods have identical signatures, i.e. have equal parameter types (as determined by types=) and rest parameter flag. #f is returned otherwise.

(method= (method ((next: next)(<number> x)(<number> y) . rest) (next x y))
         (method ((<number> a)(<number> b) . rest) (+ x y)))
  ;=> #t
(method= (method ((<number> x)(<number> y) . rest) (+ x y))
         (method ((<number> a)(<number> b)) (+ x y)))
  ;=> #f
(method= (method ((<number> x)(<value> y)) (+ x y))
         (method ((<number> a)(<number> b)) (+ x y)))
  ;=> #f
              

Miscellaneous

Generic Procedure Combination

Generic procedure combination merges the method lists of multiple generic procedures. The typical scenario for using this features is when several modules have defined generic procedure (and procedures using these generic procedures) that perform identical operations but on different data types. Generic procedure combination extends to coverage of the individual generic procedures and the dependent procedures to the combined set of data types. Furthermore, the coverage of the dependent procedures is implicitly extended to the combined set of data types.

The following example illustrates how generic procedure combination can be used to combine the functionality of two p-append procedures defined independently by two modules. It also shows how generic procedure combination implicitly extends the coverage of the p-reverse-append and p-repeat procedures defined by the modules.

(import* misc compose)
(module foo
    (p-append p-reverse-append)
  (define (p-reverse-append . args)
    (apply p-append (reverse args)))
  (define-generic p-append)
  (define-methods p-append
    [((<list> x) . rest)
     (apply append x rest)]
    [((<vector> x) . rest)
     (list->vector (apply append
                          (vector->list x)
                          (map vector->list rest)))]))
(module bar
    (p-append p-repeat)
  (define (p-repeat n x)
    (let loop ([res '()]
               [n   n])
      (if (= n 0)
          (apply p-append res)
          (loop (cons x res) (- n 1)))))
  (define-generic p-append)
  (define-methods p-append
    [((<string> x) . rest)
     (apply string-append x rest)]
    [((<symbol> x) . rest)
     (string->symbol (apply string-append
                            (symbol->string x)
                            (map symbol->string rest)))]))

(import* foo (p-append1 p-append) p-reverse-append)
(import* bar (p-append2 p-append) p-repeat)
(define-generic p-append p-append1 p-append2)
(define-method (p-append (<procedure> x) . rest)
  (apply compose x rest))

(p-append '(a b))  ;=> '(a b)
(p-append '(a b) '(c d) '(e f))  ;=> '(a b c d e f)
(p-append '#(a b))  ;=> '#(a b)
(p-append '#(a b) '#(c d) '#(e f))  ;=> '#(a b c d e f)
(p-append "ab")  ;=> "ab"
(p-append "ab" "cd" "ef")  ;=> "abcdef"
(p-append 'ab)  ;=> 'ab
(p-append 'ab 'cd 'ef)  ;=> 'abcdef
((p-append car cdr cdr cdr) '(1 2 3 4))  ;=> 4

(p-reverse-append "ab" "cd" "ef")  ;=> "efcdab"
(p-repeat 3 '(a b))  ;=> '(a b a b a b)
((p-reverse-append cdr cdr cdr car) '(1 2 3 4))  ;=> 4
((p-repeat 3 cdr) '(1 2 3 4))  ;=> (4)
          

Scoping Rules

Generic procedures are lexically scoped, but their methods are not. Hence defining methods in a local scope is generally a bad idea. One exception are module definitions. It is perfectly safe for modules to define private (i.e. not exported) generic procedures and add methods to them without interfering with other modules. However, care must be taken when generic procedures are imported or exported - methods are added to generic procedures when the module gets defined rather then when it gets imported.

The following example illustrates the scoping rules.

(define-generic m)
(define-method (m (<value> v)) v)
(m 1)  ;=> 1
(let ([x 1])
  (define-method (m (<number> v)) (+ x v))
  (m 1))  ;=> 2
(m 1)  ;=> 2

(module foo
    (m)
  (define-generic m)
  (define-method (m (<value> v)) v))
(import foo)
(m 1)  ;=> 1
(module bar
    ()
  (import foo)
  (define-method (m (<number> v)) (+ 1 v)))
(m 1)  ;=> 2
          

Method Selection

When generic procedures are invoked they select the most specific applicable method and call it, with the remaining applicable methods being made available to the invoked method via the next:.

Method applicability is determined on the basis of the types of the parameters passed in the invocation of the generic procedure. A method is applicable to a list of parameter types if and only if the following conditions are met:

  • If the method accepts rest arguments then the length of the list of parameter types must be equal or greater than the method arity (as returned by method-arity).

  • If the method does not accepts rest arguments then the length of the list of parameter types must be equal to the method arity (as returned by method-arity).

  • All the types in the method's type signature (as returned by method-types) must be super-types of the corresponding parameter types. This comparison is performed using the types<= procedure.

This algorithm is encapsulated by the method-applicable? procedure.

Method specificity is an ordering relation on applicable methods with respect to a specific list of parameter types. Informally, the relative specificity of two methods is determined by performing a left-to-right comparison of the type signatures of the two methods and the parameter types using compare-types, returning the result of the type comparison at the point of the first discernable difference.

More formally, the relative specificity of two applicable methods is computed by a triple-wise comparison on successive elements of the method signatures (as returned by method-types) and actual parameter types, using compare-types, such that

  • If we run out of elements in both method signatures then

    • If both or neither method return rest arguments (as determined by method-rest?) then the methods are of equal specificity.

    • If the first method takes rest arguments (as determined by method-rest?) then the first method is less specific than the second.

    • If the second method takes rest arguments (as determined by method-rest?) then the first method is more specific than the second.

  • If we run out of elements in the first method's signature only then the first method is less specific than the second.

  • If we run out of elements in the second method's signature only then the first method is more specific than the second.

  • If compare-types returns 'equal we proceed to the next triple.

  • If compare-types returns 'less-specific then the first method is less specific than the second.

  • If compare-types returns 'more-specific then the first method is more specific than the second.

This algorithm is encapsulated by the compare-methods procedure.

The method form and derived forms (i.e. define-method and define-methods) permit the specification of a special first parameter to the method invocation. When a generic procedure invokes a method, this parameter is bound to a procedure that when called will invoke the "next best matching" method. This is the next method in the list of applicable methods returned by applicable-methods when it was called by the generic procedure upon invocation.

If no "next best matching" method exists, i.e. the current method is the last in the list, then the next parameter is #f. This allows methods to invoke the next best matching method selectively depending on whether it is present. This is an important feature since the dynamic nature of method selection makes it impossible to determine at the time of writing the method whether there is going to be a next best matching method.

The next best matching method must be invoked with arguments to which the current method is applicable.

The following example illustrates the method selection algorithm, and use of the next: parameter:

(define-generic m)
(define-methods m
  [((next: next) (<number> x) (<value> y) (<number> z) . rest)
   (cons 'a (if next (apply next x y z rest) '()))]
  [((next: next) (<number> x) (<value> y) (<number> z))
   (cons 'b (if next (next x y z) '()))]
  [((next: next) (<number> x) (<number> y) . rest)
   (cons 'c (if next (apply next x y rest) '()))]
  [((next: next) (<number> x) (<number> y) (<value> z))
   (cons 'd (if next (next x y z) '()))])
(m 1 1 1)  ;=> '(d c b a)
(m 1 1)  ;=> '(c)
(m 1 'x 2)  ;=> '(b a)
(m 1 1 'x)  ;=> '(d c)
(m 1 'x 'x)  ;=> error
          

Object System

Requires: (import oo)

Programming in the SISC object system usually entailse the definition of generic procedures, so typically one also has to (import generic-procedures) .

The key features of the object system are:

  • class-based, with a restricted form of multiple inheritance

  • instance variables (aka slots) are accessed and modified via generic procedures

  • generic procedures implement all behaviour; there is no separate notion of methods

  • introspection API

  • complete integration into SISC's extensible type system

The examples in this section follow a few naming conventions:

Classes are types in the SISC type system and therefore class names follow the naming convention for type names (see the section called “Type System”), for example <foo-bar-baz>.
Generic procedures whose sole purpose it is to access slots of objects have names starting with : and otherwise follow the usual Scheme identifier naming conventions, i.e. all lower-case with dashes for separating words. For example :foo-bar-baz. This helps to visually distinguish slot access from ordinary procedure invocations and avoids name clashes with other procedures.
Generic procedures whose sole purpose it is to modify slots of objects, are named after the corresponding accessor procedure (whether that exists or not) with a ! appended, thus following the usual Scheme convention of denoting procedures that perform mutations on their arguments. For example :foo-bar-baz!.

Classes

Classes have a name, a list of direct superclasses, and a list of direct slot descriptions. All classes are instances of the type <class> and are themselves types in SISC's extensible type system. All classes are direct or indirect sub-classes of the class <object>, except for <object> itself, which has no super-classes.

Classes are created as follows:

procedure: (make-class symbol class-list slot-list [guid-symbol]) => class

Creates a class named symbol with the classes in class-list as its direct super-classes. See the section called “Inheritance” for restrictions on super-classes. When no super-classes are specified, the superclass is <object>.

slot-list is a list of slot names (symbols).

Slots are inherited by sub-classes. For details on slot inheritance see the section called “Inheritance”.

If guid-symbol is specified then the new class is non-generative: if guid-symbol is already bound to a class then that class is modified, instead of a new class being created. Non-generative classes are serialised specially such that deserialising them also performs this check. By contrast, deserialisation of ordinary, generative classes and their instances results in duplicate types being created, which is usually not desirable.

(define-generics :x :y :y!)
(define <foo> (make-class '<foo> '() '()))
(define <bar> (make-class '<bar> '() '()))
(define <baz> (make-class '<baz> (list <foo> <bar>)
                          '(x y)))
              

syntax: (define-class name-and-supers slot-def ...)

where name-and-supers is of the form (class-name super-class ...)
and slot-def is of the form ( slot-name [accessor [modifier]] )

Binds class-name to a newly created class.

This form expands into a definition with call to make-class on the right hand side.

slot-name names a slot. accessor must be a generic procedure. An accessor method for the slot will be added to it. modifier must be a generic procedure. A modifier method for the slot will be added to it.

(define-generics :x :y :y!)
(define-class (<foo>))
(define-class (<bar>))
(define-class (<baz> <foo> <bar>)
  (x :x)
  (y :y :y!))
              

syntax: (define-nongenerative-class name-and-supers guid slot-def ...)

This is the same as define-class, except that the resulting class is non-generative with guid, a symbol, as the unique identifier. The significance of this is explained in make-class.

One can test whether a particular value is a class:

procedure: (class? value) => #t/#f

Returns #t if value is a class, #f otherwise.

(class? (make-class '<foo> '() '()))  ;=> #t
(class? (lambda (x) x))  ;=> #f
(define-class (<foo>))
(class? <foo>)  ;=> #t
              

The following procedures provide access to the various elements of the <class> abstract data type:

procedure: (class-name class) => symbol

Returns the name of the class class.

(class-name (make-class '<foo> '() '()))  ;=> '<foo>
(define-class (<foo>))
(class-name <foo>)  ;=> '<foo>
              

procedure: (class-direct-superclasses class) => class-list

Returns the list of direct super-classes of the class class.

(define-class (<foo>))
(define-class (<bar>))
(define-class (<baz> <foo> <bar>))
(map class-name (class-direct-super-classes <foo>)  ;=> '())
(map class-name (class-direct-super-classes <baz>)  ;=> '(<foo> <bar>))
              

procedure: (class-direct-slots class) => slot-list

Returns the list of descriptions of the direct slots of the class class.

Slot descriptions are created by make-class for each slot definitions. The procedures operating on slot descriptions are documented in the section called “Slots”.

(class-direct-slots (make-class '<foo> '() '()))  ;=> '()
(define-generics :x :y :y!)
(define-class (<baz>)
  (x :x)
  (y :y :y!))
(class-direct-slots <baz> ;=> (<slot> <slot>))
              

Slots

Slot descriptions are instances of the abstract data type <slot>. They are created implicitly when classes are created; it is not possible to create them directly.

procedure: (slot? value) => #t/#f

Returns #t if value is a slot description, #f otherwise.

(define-class (<baz>) (x) (y))
(map slot? (class-direct-slots <baz>))  ;=> '(#t #t)
              

procedure: (slot-name slot) => symbol

Returns the name of the slot described by slot. This is the name given to the slot when its class was created.

(define-class (<baz>) (x) (y))
(map slot-name (class-direct-slots <baz>))  ;=> '(x y)
              

procedure: (slot-class slot) => class

Returns the class to which the slot description slot belongs.

(define-class (<baz>) (x) (y))
(eq? (slot-class (car (class-direct-slots <baz>))) <baz>)  ;=> #t
              

Slot definitions can produce procedures that allow access and modification of slots. Since slots can be defined without accessors and modifiers, this may be the only way to access/modify a particular slot.

procedure: (slot-accessor slot) => procedure

Returns a procedure than when applied to an instance of slot's class, will return the slot's value on that instance.

(define-class (<baz>) (x))
(define baz (make <baz>))
((slot-accessor (car (class-direct-slots <baz>))) baz) ;=> 1
              

procedure: (slot-modifier slot) => procedure

Returns a procedure than when applied to an instance of slot's class and a value, will set the slot's value on that instance to the supplied value.

(define-generics :x :x!)
(define-class (<baz>) (x :x))
(define baz (make <baz>))
(:x baz)  ;=> 1
((slot-modifier (car (class-direct-slots <baz>))) baz 2)
(:x baz)  ;=> 2
              

In addition to procedures for slot access and modification, slot definitions can also produce equivalent methods. These are suitable for adding to generic procedures and can, for instance, be used to achieve the same effect as specifying generic procedures for slot access/modification at class creation time.

procedure: (slot-accessor-method slot) => method

This is the same as slot-accessor, except it returns a method instead of a procedure.

procedure: (slot-modifier-method slot) => method

This is the same as slot-modifier, except it returns a method instead of a procedure.

Instantiation

Class instantiation is a two-stage process. First a new object is created whose type is that of the instantiated class. Then the initialize generic procedure is called with the newly created instance and additional arguments. initialize serves the same purpose as constructors in other object systems.

All the phases of class instantiation are carried out by a single procedure:

procedure: (make class value ...) => instance

Creates a new object that is an instance of class. The instance and values are then passed as parameters to a call to the initialize generic procedure. The result of calling make is the instance.

(define-generics :x :x!)
(define-class (<baz>)
  (x :x :x!))
(define-method (initialize (<baz> b) (<number> x)) (:x! b x))
(define baz (make <baz> 2))
(:x baz)  ;=> 2
              

By default the initialize contains a no-op method for initialising objects of type <object> with no further arguments. Since all objects are instances of <object> all classes can be instantiated by calling (make class).

(define-generics :x :x!)
(define-class (<baz>)
  (x :x :x!))
(define baz (make <baz>))
(:x baz)  ;=> #f
          

Inheritance

Inheritance is a form of sub-typing. A class is a sub-type of all its direct and indirect superclasses. Method selection in generic procedures (and hence also slot access/modification) is based on a relationship called class precedence; a total order of classes based on the partial orders established by the direct super-classes:

procedure: (class-precedence-list class) => class-list

Returns the total order of class and all direct and indirect super-classes, as determined by the partial orders obtained from calling class-direct-superclasses.

The ordering of classes returned by class-direct-superclasses is considered "weak" whereas the ordering of the class itself to its direct super-classes is considered strong. The significance of this is that when computing the class precedence list weak orderings are re-arranged if that is the only way to obtain a total order. By contrast, strong orderings are never rearranged.

(define-class (<foo>))
(define-class (<bar> <foo>))
(define-class (<baz> <foo>))
(define-class (<boo> <bar> <baz>))
(define-class (<goo>))
(define-class (<moo1> <bar> <baz> <goo>))
(map class-name (class-precedence-list <moo1>))
  ;=> (<moo1> <bar> <baz> <goo> <foo> <object>)
(define-class (<moo2> <bar> <baz> <foo> <goo>))
(map class-name (class-precedence-list <moo2>))
  ;=> (<moo1> <bar> <baz> <foo> <goo> <object>)
(define-class (<moo3> <baz> <bar> <boo>))
(map class-name (class-precedence-list <moo3>))
  ;=> (<moo3> <boo> <baz> <bar> <foo> <object>)
              

For any two classes in the class precedence list that have direct slots, one must be a sub-class of the other.

In effect this enforces single inheritance of slots while still giving control over the order of classes in the class-precedence list.

Slots from a superclass are only ever inherited once, regardless of the number of paths in the inheritance graph to that class.

If a sub-class defines a slot with the same name as one of its super-classes, instances of the resulting class ends up with two slots of the same name. If the slots are define with different accessors and modifiers then this does not cause any problems at all. If they are not then an invocation of the accessor/modifier will acess/modify the slot of the sub-class. Note that it is still possible to access/modify the slot of the superclass by using procedures/methods obtained from the slot descriptor. See the section called “Slots”.

The following example illustrates the rules governing slot inheritance.

;;ordinary slot access and modification
(define-generics
  :x :x! :y :y! :z :z!
  :xb :xb! :yb :yb! :zb :zb!)
(define-class (<foo>)
  (x :x :x!)
  (y :y :y!)
  (z :z))
(define f (make <foo>))
(:x f)  ;=> #f
(:y f)  ;=> #f
(:z f)  ;=> #f
(:x! f 2)
(:y! f 2)
(:x f)  ;=> 2
(:y f)  ;=> 2

;;overloading slots in sub-class
(define-class (<bar> <foo>)
  (x :x :x!)
  (y :yb :yb!)
  (zb :zb :zb!))
(define b (make <bar>))
(:x b)  ;=> #f
(:y b)  ;=> #f
(:z b)  ;=> #f
(:yb b)  ;=> #f
(:zb b)  ;=> #f
(:y! b 3)
(:yb! b 4)
(:y b)  ;=> 3
(:yb b)  ;=> 4

;;accessing a fully shadowed slot
(define :foo-x
  (cdr (assq 'x (map (lambda (x)
                       (cons (slot-name x) (slot-accessor x)))
                     (class-direct-slots <foo>)))))
(:foo-x b)  ;=> 1

;;inheritance restriction on slots
(define-class (<boo> <baz>)
  (yb :yb :yb!))
(define-class (<goo>))
(define-class (<moo> <bar> <boo> <goo>))  ;=> error