Table of Contents
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.
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 hooktype-of
returns a type based on the Java type of the internal representation ofvalue
.
(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 oftype2
.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
andtype2
with respect totype3
.type3
must be a sub-type oftype1
andtype2
.type1
andtype2
are first compared usingtype<=
. If that comparison indicates that the types are disjoint (i.e.type1
is not sub-type oftype2
,type2
is not a sub-type oftype1
and the types are not equal) then additional information fromtype3
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 oftype1
withtype2
usingtype<=
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
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 oftype
.The predicate obtains
value
's type usingtype-of
and then compares it totype
usingtype<=
.
(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 intype-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 intype-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 intype-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 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)])))
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 ofsisc.data.Value
, the base of the SISC value type hierarchy.
(define <record> (make-type '|sisc.modules.record.Record|)) (type<= <record> <value>) ;=> #t
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:
definition of the generic procedure
adding of methods to the generic procedure
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.
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 formname
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)
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 istype-list
. Ifrest?
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. Henceprocedure
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 alambda
.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 themake-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
togeneric-procedure
. Any existing method with the same signature asmethod
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
togeneric-procedure
. Any existing method with the same signature as one of the methods inmethod-list
is are removed. When several methods inmethod-list
have the same signature, only the last of these methods is added.Method addition is thread-safe. Calling
add-methods
instead ofadd-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>)
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 bymethod-applicable?
to parameters of the types specified intype-list
. The methods are returned ordered by their specificity, determined by pair-wise comparison usingcompare-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 intype-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 totype-list
, as determined bymethod-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 bybinding
. Both forms may be used in a given call tolet-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 inlet
as well, in order using an implicitbegin
.
(let-monomorphic ([foo-generic <number> <string>] [(bar bar-generic) <char>]) (foo-generic 3 "four") (bar #\x))
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 specialnext:
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
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)
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
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
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 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 inclass-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: ifguid-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 withguid
, a symbol, as the unique identifier. The significance of this is explained inmake-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>))
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.
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 andvalue
s are then passed as parameters to a call to theinitialize
generic procedure. The result of callingmake
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 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 callingclass-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