Description

A prototype based object system with multiple object dynamic dispatch

Author

Thomas Chust

Version

Usage

(require-extension modds)

Download

modds.egg

Documentation

The modds egg provides yet another object system for CHICKEN. It tries to be lightweight and to combine the advantages of a prototype based system with those of tinyclos' dynamic dispatch system for methods.

With modds everything is an object, there are no classes and single inheritance of data and behaviour is implemented through a prototype link in every object record which is set when the object record is created as a "clone" of some other object. The prototype of an object can also be changed at runtime.

Similar to other prototype based object systems, modds stores data slots of every object record as a hash table mapping symbols to values. If a slot value is requested for a non-primitive object and it is not found in the object itself, its prototype chain is searched for a value.

Dissimilar to many other prototype based object systems, methods of objects are not stored in slots. Each object record stores a hierarchy of hash tables which in fact maps combinations of a method name and argument position of the object to a list of possible methods.

In this way it is possible to dispatch methods dynamically on each of their arguments simultaneously, not just on the first one, as it would be the case if they were stored in a single object slot. It is also possible to dispatch to "super" methods implementation from inside a method implementation. A disadvantage, though, is the fact, that methods with rest argument lists are not supported. On the other hand there is support for optional and keyword parameters, which is sufficient most of the time.

The mechanics behind modds were inspired by the object system of the Slate programming language.

High level syntax and procedures

procedure: (object (prototype <object or symbol>) slots...) => <object>

Creates a clone of the given prototype, which may be specified either as an object or a symbol globally bound to an object.

If slot definitions are present, each of them can be either a symbol or a list of a symbol and a value. All given slots are set to their given values or #f in the new object.

macro: (define-object (name <symbol>) slots...) => <void>
macro: (define-object ((name <symbol>) (prototype <object or symbol>)) . slots) => <void>

Macro syntax wrapping the object procedure. If no prototype is specified, <object> is used.

The symbol name is defined to hold the new object and the name slot of the new object is set to name. Refer to the description of the name slot for an explanation why this is useful.

syntax: #,(object (prototype <object or symbol>) slots...) => <object>

Read syntax wrapping the object procedure.

macro: (-> (object <object>) (slot <symbol>)) => <root>

References the slot specified by the symbol slot in the object object. The slot name is automatically quoted.

macro: (=> (object <object>) (slot <symbol>) (value <root>)) => <void>

Sets the slot specified by the symbol slot in the object object to the value value. The slot name is automatically quoted.

macro: (~> (object <object>) (slot <symbol>) (proc <procedure>)) => <void>

Updates the slot specified by the symbol slot in the object object to the value obtained by applying proc to its current value. The slot name is automatically quoted.

Equivalent to (=> object slot (proc (-> object slot))) but more efficient.

macro: (++> (object <object>) (slot <symbol>)) => <number>
macro: (>++ (object <object>) (slot <symbol>)) => <number>

Pre- or post-increments the slot specified by the symbol slot in the object object and returns its value after or before the increment. The slot name is automatically quoted.

macro: (--> (object <object>) (slot <symbol>)) => <number>
macro: (>-- (object <object>) (slot <symbol>)) => <number>

Pre- or post-decrements the slot specified by the symbol slot in the object object and returns its value after or before the increment. The slot name is automatically quoted.

macro: (define-generic (name <symbol>)) => <void>

Creates a new generic method stub that calls the method implementation with the given name which matches its arguments. Binds the stub to the variable name.

macro: (define-method ((name <symbol>) arguments...) expressions...) => <void>

Adds a method definition for the given name to the prototypes specified in the arguments list. Each element of the arguments list can either be a symbol or a list of a symbol and a prototype. Specifying only the symbol is equivalent to specifying a prototype of <root>.

The new method will execute the given expressions. Inside the new method, the symbols specified in the arguments list are bound to the arguments (of matching types) and the symbol call-next-method is bound to a procedure that redispatches the method call to the next less specific or less recently defined method implementation also matching the argument types.

If a generic method is called on argument types for which no method implementation has been defined, an error is signalled. Variadic methods are not supported.

Slots and methods for all objects

slot: (-> <root> name) => <symbol>

The name of an object. If an object is printed and this slot holds a symbol which is globally bound to the object itself, then just the symbol is printed.

method: (is-a? (object <root>) (prototype <root>)) => <boolean>

Checks whether the given object was cloned directly or indirectly from the given prototype.

method: (clone (prototype <object>)) => <object>
method: (clone (prototype <primitive>)) => <primitive>

Creates a new object from a given prototype.

If the prototype is an object record, it is not copied, but instead a new object with an appropriate prototype link is created. If the prototype is a primitive, though, it is copied using object-copy.

Low level procedures

procedure: (object? (object <root>)) => <boolean>

Checks whether the given object is an object structure created by this object system or whether it is of a primitive type.

getter: (prototype (object <root>)) => <object>
setter: (prototype-set! (object <object>) (prototype <object>)) => <void>
setter: (set! (prototype (object <object>)) (prototype <object>)) => <void>

Retrieves or sets the prototype of an object. The prototype can only be set for object records, but every object, even primitive ones, have a prototype that can be retrieved.

Of course the prototypes returned for primitive objects are all determined by checking a bunch of type predicates and then returning an appropriate constant.

getter: (slot-ref (object <object>) (name <symbol>) #!optional (default <procedure>)) => <root>
setter: (slot-set! (object <object>) (name <symbol>) (value <root>)) => <void>
setter: (set! (slot-ref (object <object>) (name <symbol>)) (value <root>)) => <void>

Retrieves or sets the value of a slot in the given object record. Primitive values have no slots that could be set or retrieved.

If the default parameter is specified, it is called if no matching slot is found in the given object record or its prototype chain instead of signalling an error.

procedure: (slot-update! (object <object>) (name <symbol>) (proc <procedure>)) => <void>

Updates a slot in the given object to the value obtained by applying proc to its current value.

Equivalent to (slot-set! object name (proc (slot-ref object name))) but more efficient.

procedure: (remove-slot! (object <object>) (name <symbol>)) => <boolean>

Removes any definition for the slot with the given name from the given object. Returns #t if something was actually removed, #f if no such slot was present anyway.

procedure: (push-method-for-role! (object <object>) (name <symbol>) (n <integer>) (method <procedure>)) => <void>
procedure: (drop-method-for-role! (object <object>) (name <symbol>) (n <integer>)) => <boolean>

Registers a new method implementation method, or unregisters the most recently defined method implementation, that can be called by the given name with object as its nth parameter.

When dropping method implementations, the return value indicates whether something was actually dropped or whether no such method existed anyway.

procedure: (methods-for-role (object <object>) (name <symbol>) (n <integer>)) => <list of procedure>

Instead of using this procedure directly, consider using the standard method dispatch system by calling call-method.

Retrieves a list of all method implementations that have the given name and can take the given object as their nth argument. The list will start with the most specific, most recently defined method that matches the role.

procedure: (method-invocation (name <symbol>) (methods <list of procedure>) (params <list of root>)) => <procedure>

Instead of using this procedure directly, consider using the standard method dispatch system by calling call-method.

Constructs a procedure of no arguments, which when called will invoke the first element in the list methods with the given params. If methods is the empty list, an error saying no appropriate method called name was found is thrown.

The invoked method will be passed a call-next-method argument which invokes the second element in the list methods as if by calling ((method-invocation name (cdr methods) params)).

methods should be a list returned by a call to methods-for-role or an intersection of such lists.

procedure: (call-method (name <symbol>) (arguments <list of root>)) => <root>

Finds the most specific, most recently defined method implementation for the given name that matches the given arguments and calls that procedure.

Predefined prototypes

The following hierarchy of prototypes is predefined and represents mostly classes of builtin primitive types. Your own objects should be derived from <object>, but you can define method implementations that specialize their arguments on one of the primitive prototypes to dispatch to those methods when primitive arguments are passed to them.

   <root>
     <object>
     <primitive>
       <void>
       <null>
       <boolean>
       <symbol>
       <char>
       <vector>
       <pair>
       <number>
         <integer>
	   <exact>
	 <inexact>
       <string>
       <procedure>
       <port>
         <input-port>
	 <output-port>
       <pointer>
         <tagged-pointer>
	 <swig-pointer>
       <locative>
       <byte-vector>
         <u8vector>
	 <s8vector>
	 <u16vector>
	 <s16vector>
	 <u32vector>
	 <s32vector>
	 <f32vector>
	 <f64vector>
       <structure>
         <char-set>
	 <condition>
	 <condition-variable>
	 <continuation>
	 <environment>
	 <array>
	 <hash-table>
	 <lock>
	 <mmap>
	 <mutex>
	 <promise>
	 <queue>
	 <read-table>
	 <regexp>
	 <tcp-listener>
	 <thread>
	 <time>
       <end-of-file>

Examples

Small example:

;;;; modds-demo.scm
;;;; An example of the modds object system syntax

(require-extension (srfi 26) modds)

(define-object <point>
  x
  y)

(define-object (<rect> <point>)
  (width 0)
  (height 0))

(define-generic make)

(define-method (make (proto <point>) #!key ((x <number>) 0) ((y <number>) 0))
  (let ((p (clone proto)))
    (=> p x x)
    (=> p y y)
    p))

(define-generic resize)

(define-method (resize (r <rect>) #!optional ((w <number>) 0) ((h <number>) 0))
  (~> r width (cut + <> w))
  (~> r height (cut + <> h))
  r)

(define-generic inside?)

(define-method (inside? (p <point>) (r <rect>))
  (let ((dx (- (-> p x) (-> r x)))
	(dy (- (-> p y) (-> r y))))
    (and (>= dx 0)
	 (>= dy 0)
	 (<= dx (-> r width))
	 (<= dy (-> r height)))))

(define-method (inside? (r0 <rect>) (r1 <rect>))
  (and
   (call-next-method)
   (let ((p (clone <point>)))
     (=> p x (+ (-> r0 x) (-> r0 width)))
     (=> p y (+ (-> r0 y) (-> r0 height)))
     (inside? p r1))))

(define r
  '#,(object <rect> (x 1) (y 2) (width 3) (height 4)))

(print
 (inside? (make <point> y: 3 x: 2) r))
(print
 (inside? (make <point> x: 5 y: 3) r))
(print
 (inside? (make <point> y: 7 x: 2) r))

(print
 (inside? '#,(object <rect> (x 2) (y 3) (width 1) (height 1)) r))
(print
 (inside?
  (let ((r1 '#,(object <rect> (x 8) (y 8))))
    (++> r1 width)
    (++> r1 height)
    r1)
  r))
(print
 (inside? (resize (make <rect> x: 2 y: 3) 10) r))

License

Copyright (c) 2006, Thomas Chust <chust@web.de>.  All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer. Redistributions in
  binary form must reproduce the above copyright notice, this list of
  conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution. Neither the name of the
  author nor the names of its contributors may be used to endorse or
  promote products derived from this software without specific prior
  written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.