Title

Simple object system.

Author

Christian Queinnec

Status

This SRFI is currently in ``withdrawn'' status. To see an explanation of each status that a SRFI can hold, see here. You can access the discussion via the archive of the mailing list.

Abstract

This SRFI presents an object system to define classes, generic functions as well as to support some level of introspection. This object system is based on Meroon-V3 which is itself inspired by CLOS. Meroon-V3 is distributed and used since 1992.

Issues

There are two kinds of object models: the closed model or the open model. The closed model comes from statically typed systems (C++, Java, OCaml, etc.); this model is characterized by the following dogmas: classes are encapsulation units, methods must be known at class-definition time, there's only one receiver for a message (since generic functions are hard to type). This model has good compilation properties since most types are known at compile-time.

The open model is more dynamic (CLOS, Meroon-V3, STklos). It acknowledges the fact that all behaviors cannot be expressed at class-definition time, that it should be possible to add new behaviors to existing instances (data, particularly if persistent, often pre-exist to treatments), that introspection is desirable and useful for serialization, debug and all sorts of walkers, that some way to customize or extend the implementation should also exist as well.

This document proposes an open model in the dynamic spirit of Scheme (and Lisp). Meta-object aspects are not considered in this proposal but are still possible.

Rationale

Objects are useful and trendy (at least before components come). Meroon-V3 has proved useful for many years, has been ported to (no particular order intended) mzscheme, scheme->C, elk, bigloo, scm, guile, oscheme, mit-scheme, gambit. It is proposed to build on its strengths.

This model makes possible that vectors be regular instances of the object system since the object system allows fields to be regular (ie Mono-Field (a Mono-Field only holds one value)) or indexed (ie Poly-Field, the number of values it may hold is fixed at instantiation-time). Vectors are regular objects with a single indexed field.

The proposed model is based on simple inheritance (for the same reasons Java took).

Specification

The documentation of Meroon-V3 (23 pages) is available on the net. The following interface stresses the features that are within the scope of the present document.

The following are new special-forms. They use keywords ie symbols prefixed with a colon.

(define-class class-name superclass-name
  (field-specification ...)
  ;; where field-specification is
  ;; ([ = | * ] field-name 
  ;;            [ :mutable | :immutable |
  ;;              :initialize (lambda () form) |      ; for (=) Mono-Field
  ;;              :initialize (lambda (index) form)   ; for (*) Poly-Field
  ;;             ] )
  [ :prototype ] )                                    ; non-instantiable class

(define-generic (generic-name variable ...)
  ;; where variable is
  ;;    symbol                                   ; regular variable
  ;;  | (symbol [ class-name ])                  ; discriminating variable
  [ default-body ] )

(define-method (generic-name variable ...)
  ;; where variable is
  ;;    symbol                     ; regular variable
  ;;  | (symbol class-name)        ; discriminating variable
  body                             ; (call-next-method) allowed
  )

(instantiate class-name 
  :mono-field-name form
  :poly-field-name form ...
  :poly-field-length size
  ... ) 

Class definition

A define-class form creates a new class (redefinition of classes is left unspecified). The first parameter is the name of the class, the second is the name of the super-class, specifications of fields appear as third parameter, additional parameters may appear after. Except initializers that are evaluated, no parameter or expressions within parameters are evaluated.

A class is made of fields which may be regular or indexed. A field may be mutable or not (mutable by default). A default initializer may be specified (a thunk for a regular field, a unary function for an indexed field). The form that define these initializers are evaluated at class-definition time. The :prototype class option forbids instantiation of the class.

When a class is defined, some functions are created: a predicate and some read/write accessors.

Example

The following class definition creates the following functions: Foo?, Foo-bar, Foo-hux, set-Foo-hux! and Foo-hux-length. A variable Foo-class is also created that holds a (possibly indirect) instance of Class for introspection purpose.

(define-class Foo Object
  ((= bar :immutable)
   (* hux :initializer (lambda (i) i)) ) )

Here is the signature of the generated functions:

(Foo? anySchemeValue) -> boolean
(Foo-bar aFooInstance) -> anySchemeValue
(Foo-hux aFooInstance aNaturalNumber) -> anySchemeValue
(set-Foo-hux! aFooInstance anySchemeValue aNaturalNumber) -> unspecified
(Foo-hux-length aFooInstance) -> aNaturalNumber

Predicates, such as Foo? may be invoked on any Scheme values. Accessors may only apply on (possibly indirect) instances of Foo. Writers return an unspecified value. Accessors whether readers or writers when applied on an indexed field take an index as last argument.

Generic definition

The define-generic form defines generic functions. When a generic function is defined, it does not possess any method. However it may have a default body which is invoked when no specific method is found (for instance, on values that are not objects). The generic function must specify the discriminating variables (at least one). Syntactically a discriminating variable appears within parentheses. If a class is associated to a discriminating variable, it restricts the set of methods that may be adjoined to this generic function (for instance, the following clone function may only have methods defined on subclasses of Object). A generic function may have more than one discriminating variable.

The following are predefined generic functions. New methods may be added at will by programmers.

(define-generic (initialize! (o Object)) ...)
(define-generic (clone (o Object)) ...)

The initialize! function is automatically invoked on every freshly created instance. It may be used to register the instance in some data structures.

The clone function, by default, performs a shallow copy of an object.

Example

A generic predicate werk? may be defined on all (direct or indirect) instances of Object as:

(define-generic (werk? (o))
  #f )

Method definition

The define-method form adjoins a method to a generic functions. Methods may be adjoined to any generic functions provided that the method has a compatible signature ie the same number of discriminating variables at the same position associated to classes that are sub-classes of the classes mentioned in the generic function definition. If the generic function has a final dotted variable, then methods should also have such a final dotted variable.

Within the body of a method, it is possible to use the (call-next-method) local special form. It is not possible to use it elsewhere. The next method is defined to be the method that would have been invoked if the current method was absent. It is forbidden to define ambiguous multi-method that is a method that will make some signature ambiguous. This problem may only arise on methods that have strictly more than one discriminating variable: is SC is a subclass of C and a method M1 is defined on SC * C then it is not possible to define a method M2 on C * SC since that would make a call on SC * SC ambiguous. However if a method M3 on SC * SC is defined and does not contain a call to (call-next-method) then it is then possible to define a method M2 on C * SC. [NOTE: that may be considered as too dynamic, observe however that call-next-method is not a variable.]

Example

If all (direct or indirect) instances of Foo satisfy the werk? predicate, then one may write:

(define-method (werk? (o Foo))
  #t ) 

Instantiation

New objects may be created with the instantiate special form. This form takes as first parameter the name of a class. The other parameters specify the initial content of the object. The initial content of a field is specified by a keyword whose name is the name of the field to fill (prefixed with a colon). If the field is regular, the keyword is followed by a single expression (to be evaluated); if the field is indexed, the keyword is followed by as many expressions (to be evaluated) as needed. Alternatively, an indexed field may be specified by its length in which case the initial content of this indexed field is unspecified.

The fields do not need to be ordered nor to be all specified. In which case, the initializer if defined at class-definition-time is run. For a regular field, the initializer looks like a thunk; for an indexed field, the initializer is a unary function taking the index as argument. Fields are guaranteed to be filled in order starting from inherited fields. Indexed fields are filled starting from 0.

Example

Here are some instantiations of Foo:

(instantiate Foo :bar 1 :hux 2 3 4)
(instantiate Foo :hux-length 3 :bar 11)
Note that the values of the fields may be given in any order. A mono-field defined with an initializer may be omitted. A poly-field defined with an initializer only requires its length to be given.

Primitives

The following are regular functions. Note: value means whatever Scheme value; object means a (possibly indirect) instance of Object; class means a (possibly indirect) instance of Class.

(object? value)                  ; anySchemeValue -> boolean
(object->class object)           ; Object -> Class
(is-a? value class)              ; anySchemeValue * Class -> boolean
(subclass? class super-class)    ; Class * Class -> boolean

Among meta-objects only classes and fields are partially exposed not by their definition but through some functions or some variables. Class is the ``class of classes'' where classes are the values returned by the object->class function. The document does not impose Class to be definable by a specific define-class form. Classes may only be used with the functions whose name start with Class-.

Field is the class of fields, objects reifying information about fields. The document does not impose Field to be definable by a specific define-class form. Fields may only be used with the functions whose name start with Field-.

The definition of a class may be inspected via a number of functions. It is possible to access the name of a class, its superclass (#f if that does not exist) as well as its fields. Fields are reified as if instances of a class of fields: Field. The document does not impose Field to exist (as if explicitly created by define-class) provided the Field-* functions still exist.

Object-class                     ; Class
(Class? value)                   ; anySchemeValue -> boolean
(Class-name class)               ; Class -> symbol
(Class-super-class class)        ; Class -> Class
(Class-field class index)        ; Class * nat -> Field
(Field? value)                   ; anySchemeValue -> boolean
(Field-name field)               ; Field -> symbol
(Field-mutable? field)           ; Field -> boolean
(Field-initializer field)        ; Field -> closure
(Mono-Field? value)              ; anySchemeValue -> boolean
(Poly-Field? value)              ; anySchemeValue -> boolean
(field-value object field [ index ])            ; Object * Field [* nat] -> value
(set-field-value! object value field [ index ]) ; Object * value * Field [* nat] -> unspecified
(field-length object field)                     ; Object * Field -> nat

Two generic functions are predefined:

(initialize! object)             ; Object -> Object
(clone object)                   ; Object -> Object

Implementation

Meroon-V3 implements the features of this document (and much more than this in fact) but is currently 10k lines long. Meroonet is another almost conforming essential implementation (but for multi-methods) whose size makes it more readable (875 lines including comments) described in chapter 11 of my book: Lisp in Small Pieces, Cambridge U. Press, 1996.

Copyright

Copyright (C) Christian Queinnec (2000). All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Editor: Mike Sperber