For the complete documentation index, see llms.txt. This page is also available as Markdown.

(lispkit object)

Library (lispkit object) implements a simple, delegation-based object system for LispKit. It provides procedural and declarative interfaces for objects and classes. The class system is optional. It mostly provides means to define and manage new object types and construct objects using object constructors.

Introduction

Similar to other Scheme and Lisp-based object systems, methods of objects are defined in terms of object/class-specific specializations of generic procedures. A generic procedure consists of methods for the various objects/classes it supports. A generic procedure performs a dynamic dispatch on the first parameter (the self parameter) to determine the applicable method.

Generic procedures

Generic procedures can be defined using the define-generic form. Here is an example which defines three generic methods, one with only a self parameter, and two with three parameters self, x and y. The last generic procedure definition includes a default method which is applicable to all objects for which there is no specific method. When a generic procedure without default is applied to an object that does not define its own method implementation, an error gets signaled.

(define-generic (point-coordinates self))
(define-generic (set-point-coordinates! self x y))
(define-generic (point-move! self x y)
  (let ((c (point-coordinate self)))
    (set-point-coordinate! self (+ (car c) x) (+ (cdr c) y))))

Objects

An object encapsulates a list of methods each implementing a generic procedure. These methods are regular closures which can share mutable state. Objects do not have an explicit notion of a field or slot as in other Scheme or Lisp-based object systems. Fields/slots need to be implemented via generic procedures and method implementations sharing state. Here is an example explaining this approach:

(define (make-point x y)
  (object ()
    ((point-coordinates self)
      (cons x y))
    ((set-point-coordinates! self nx ny)
      (set! x nx) (set! y ny))
    ((object-description self)
      (string-append (object-description x)
                     "/"
                     (object-description y)))))

This is a function creating new point objects. The x and y parameters of the constructor function are used for representing the state of the point object. The created point objects implement three generic procedures: point-coordinates, set-point-coordinates, and object-description. The latter procedure is defined directly by the library and, in general, used for creating a string representation of any object. By implementing the object-description method, the behavior gets customized for the object.

The following lines of code illustrate how point objects can be used:

Inheritance

The LispKit object system supports inheritance via delegation. The following code shows how colored points can be implemented by delegating all point functionality to the previous implementation and by simply adding only color-related logic.

The object created in function make-colored-point inherits all methods from object super which gets set to a new point object. It adds a new method to generic procedure point-color and redefines the object-description method. The redefinition is implemented in terms of the inherited object-description method for points. The form invoke can be used to refer to overridden methods in delegatee objects. Thus, (invoke (super object-description) self) calls the object-description method of the super object but with the identity (self) of the colored point.

The following interaction illustrates the behavior:

Objects can delegate functionality to multiple delegatees. The order in which they are listed determines the methods which are being inherited in case there are conflicts, i.e. multiple delegatees implement a method for the same generic procedure.

Classes

Classes add syntactic sugar, simplifying the creation and management of objects. They play the following role in the object-system of LispKit:

  1. A class defines a constructor for objects represented by this class.

  2. Each class defines an object type, which can be used to distinguish objects created by the same constructor and supporting the same methods.

  3. A class can inherit functionality from several other classes, making it easy to reuse functionality.

  4. Classes are first-class objects supporting a number of class-related procedures.

The following code defines a point class with similar functionality as above:

Instances of this class are created by using the generic procedure make-instance which is implemented by all class objects:

Each object created by a class implements a generic procedure object-class referring to the class of the object. Since classes are objects themselves we can obtain their name with generic procedure class-name:

Generic procedure instance-of? can be used to determine whether an object is a direct or indirect instance of a given class. The last two lines above show that pt2 is an instance of point, but pt is not, even though it is functionally equivalent.

The following definition re-implements the colored point example from above using a class:

The following lines illustrate the behavior of colored-point objects vs point objects:

Procedural object interface

object-type-tag

Symbol representing the object type. The type-for procedure of library (lispkit type) returns this symbol for all objects created via object or make-object.

Every class created via make-class (or define-class) implicitly defines its own, more specific type tag for its instances, distinct from object-type-tag and class-type-tag, but not directly exposed to user code. type-of (see library (lispkit type)) returns a list of such tags for an object, ordered from most specific to least specific, e.g. (type-of pt2) ⇒ (point object) for an instance of a point class, (type-of point) ⇒ (class object) for the point class object itself, and (type-of (make-object)) ⇒ (object) for a plain object without a class.

(object? obj)

Returns #t if obj is an object as defined by this library. Objects are either created procedurally via make-object or declaratively via object.

(make-object) (make-object delegate ...)

Creates and returns a new object without any methods of its own. If one or more delegate objects are provided, the new object inherits all of their methods (see object-methods); if several delegates implement a method for the same generic procedure, the method of the delegate listed first takes precedence. make-object is the procedural counterpart of the object syntax, which additionally allows methods to be attached right away.

(method obj generic)

Returns the method implementing generic procedure generic for object obj, or #f if obj does not implement generic. The result is a plain procedure still expecting the "self" object as its first argument, e.g. ((method obj generic) obj arg ...). This is the procedure used internally to perform dynamic dispatch, both by generic procedures created with make-generic-procedure and by the invoke syntax.

(object-methods obj)

Returns an association list of all methods implemented by object obj, i.e. a list of pairs (generic . method). If add-method! was used to add more than one method for the same generic procedure, all of them show up in the list, with the most recently added one listed first (see add-method!).

(add-method! obj generic method)

Adds method as a new implementation of generic procedure generic to object obj, mutating obj in place. method is a procedure expecting the "self" object as its first argument. If obj already implements generic, the previous method is not discarded but merely shadowed: it becomes visible again once method is removed via delete-method!.

(delete-method! obj generic)

Removes the most recently added implementation of generic procedure generic from object obj, mutating obj in place, and exposing a previously shadowed implementation of generic added earlier via add-method!, if there is one. If obj does not implement generic, delete-method! has no effect.

(make-generic-procedure) (make-generic-procedure default)

Returns a new generic procedure (lambda (obj arg ...) ...) implementing dynamic dispatch on its first argument obj: if obj is an object (as defined by object?) implementing a method for this generic procedure, that method is applied to obj and the remaining arguments. Otherwise, default is applied instead, both for objects lacking an implementation of this generic procedure and for arguments that are not objects at all. If default is not provided, invoking the generic procedure in such a case signals an error. define-generic provides a more convenient, declarative way to define generic procedures.

Declarative object interface

(object ((delegatevar delegate) ...) ((generic self arg ... . rest) e1 e2 ...) ...)

Creates and returns a new object, combining make-object and add-method! into a single declarative expression. Each (_delegatevar delegate_) clause binds delegatevar to the value of expression delegate and includes it as a delegate of the new object, exactly as with make-object; delegatevar is visible in the method bodies below (and in later delegate expressions), which is how objects created with object implement inheritance (see the colored-point example in the introduction above). Each ((_generic self arg ... . rest_) _e1 e2 ..._) clause adds a method for generic procedure generic to the new object, equivalent to (add-method! obj generic (lambda (self arg ... . rest) e1 e2 ...)). Both the delegate expressions and the method bodies may refer to variables of the surrounding lexical scope, which is how objects created with object encapsulate mutable state.

(define-generic (name self arg ... . rest)) (define-generic (name self arg ... . rest) e1 e2 ...)

Defines name as a new generic procedure via make-generic-procedure. In the first form, name has no default implementation, and applying it to an object (or value) without a matching method signals an error. In the second form, (lambda (self arg ... . rest) e1 e2 ...) is used as the default implementation, applied whenever the first argument is not an object, or is an object without its own method for name.

(invoke (obj generic) self arg ...)

Invokes the method that object obj implements for generic procedure generic, passing it self and arg ... as arguments, instead of obj and arg .... This is used to call an overridden method of a delegatee object while keeping the identity of the overriding object, as illustrated by the colored-point example in the introduction above: (invoke (super object-description) self) calls the object-description method implemented by the super delegate, but passes the colored point itself, rather than super, as self. Unlike applying generic directly to obj, invoke signals an error if obj does not implement generic, since it does not fall back to a default implementation.

Procedural class interface

class-type-tag

Symbol representing the class type. The type-for procedure of library (lispkit type) returns this symbol for all class objects.

(class? obj)

Returns #t if obj is a class object, #f otherwise.

root

The root class object. All class objects have root as its direct or indirect superclass object.

(make-class name superclasses constructor)

Returns a new class whose name is name, which needs to be a symbol, or an error is signaled. superclasses is a list of superclass objects, each of which needs to satisfy class?, or an error is signaled. constructor is a procedure, or an error is signaled; it is called whenever an instance of this new class is being created via make-instance, with the arguments passed to make-instance, and it needs to return two values:

  1. A list of delegate objects, one for each class listed in superclasses, in the same order; make-instance checks that the n-th delegate satisfies (instance-of? (list-ref superclasses n) delegate), and signals an error otherwise. This is how instances of the new class inherit the methods of their superclasses (see object for how delegate objects contribute methods).

  2. An initializer procedure (lambda (instance) ...), called by make-instance with the newly created instance once its delegates have been combined and its object-class method has been set up (so that object-class and, transitively, class-name are already available within the initializer). The initializer is typically used to add-method! further methods to instance that are specific to the new class, rather than inherited from a delegate.

The define-class syntax provides a more convenient, declarative way to create classes, expanding into a call to make-class with a suitably constructed constructor procedure.

The following example defines a simple counter class directly via make-class, without any superclasses. Its constructor takes an initial count and returns the two required values: an empty list of delegates (since counter has no superclasses) and an initializer procedure that adds counter-value and counter-increment! methods to the new instance, closing over a private, mutable count variable:

Since the constructor's let is re-evaluated for every call to make-instance, each instance of counter gets its own, independent count:

This is exactly the underlying mechanism that define-class builds on: it expands into a call to make-class whose constructor validates and processes args, evaluates the object expression's delegate clauses into a list of delegates, and wraps the object expression's method clauses into an initializer procedure, closely resembling the counter example above.

Instance methods

(object-class obj)

Returns the class of object obj.

(object-equal? obj other)

Returns #t if obj and other are considered equal objects. The default implementation (used by objects and classes that do not provide their own object-equal? method) compares obj and other with equal?. Since objects internally wrap a list of methods (closures), and closures are only equal? to themselves, this default effectively behaves like an identity comparison for most objects: two separately constructed objects with equivalent state, but without a custom object-equal? method, are generally not considered equal, e.g. (object-equal? (make-instance point 1 2) (make-instance point 1 2)) ⇒ #f, even though both instances represent the same coordinates. Classes that need value-based equality should implement their own object-equal? method.

(object-description obj)

Returns a string representation of object obj. The default implementation (used by objects and classes that do not provide their own object-description method) converts obj to a string via write, e.g. (object-description (make-object)) ⇒ "#<object #<box ()>>".

Class methods

(class-name class)

Returns the class name of class.

(class-direct-superclasses class)

Returns a list of superclass objects of class.

(subclass? class other)

Returns #t if class is a subclass of class other, #f otherwise.

(make-instance class arg ...)

Creates and returns a new object of class. arg ... are the constructor arguments passed to the constructor of class.

(instance-of? class obj)

Returns #t if obj is an instance of class.

Declarative class interface

(define-class (name . args) (super ...) init ... (object ((delegatevar delegate) ...) ((generic self arg ... . rest) e1 e2 ...) ...)) (define-class (name . args) pred? (super ...) init ... (object ((delegatevar delegate) ...) ((generic self arg ... . rest) e1 e2 ...) ...))

Defines name as a new class via make-class. The constructor of the class takes args and is expressed in terms of an object expression: each (delegatevar delegate) clause becomes a delegate of instances created by this class, and each ((generic self arg ... . rest) e1 e2 ...) clause becomes a method implementation of instances of name, exactly as with the object syntax. The optional init ... expressions are evaluated first, in terms of args, before the delegates and methods are set up; they are typically used for validating constructor arguments, as illustrated by the colored-point example in the introduction above, which signals an error if the given coordinates are negative.

The second form additionally names a type predicate pred? for instances of name, meant to be equivalent to (lambda (obj) (instance-of? name obj)).

While the point/colored-point example in the introduction above shows single inheritance combined with method overriding via invoke, a class may also list several superclasses at once. In that case, the object expression needs to provide one delegate per superclass, listed in the same order as the superclasses; make-instance checks that each delegate is indeed an instance of the corresponding superclass. This is how independently defined classes can be combined via multiple inheritance, without any of the combined classes having to know about each other. The following example defines two small, independent classes, movable and colored, and then combines both into a sprite class that inherits all of their methods purely through delegation, without needing to override or re-implement anything itself:

Class sprite lists both movable and colored as superclasses, and provides one delegate for each, created via make-instance. Since sprite does not need to customize any inherited behavior, its object expression has no method clauses of its own; all of its methods are contributed entirely by its two delegates:

Instances of sprite behave exactly like instances of movable and colored for the generic procedures position, move!, and color, while object-class and class-name still correctly identify them as sprite objects, and each instance keeps its own, independent state:

Last updated