Chapter 7. Types and Objects
Prev |  | Next

# Chapter 7. Types and Objects

## 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 `value`s 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

Prev |  | Next
Chapter 6. Threads and Concurrency | Home | Chapter 8. Java Interaction