Lightweight Object Systems for Scheme

« More entries

The Scheme programming language is a minimal one – not brainfuck-like minimal, contrived and obscure, made for entertaining the twisted mind. It is a minimal language in the sense of light and form and in the concept of emergent processes, musical notes, pigments – essential materials or ideas that give birth to forms of art and construction. It is from this idealistic reductionism that my passion for this language stems. The beauty of its essential building blocks feels as the quintessential elements of the digital architecture.

But this comes at a price. Just as clay is not the material of prefabrication, light and form is not the language of mass production. A language of extreme flexibility is not a language for distribution; it’s a language for the individualistic hacker. And if you don’t agree with my digression, well, you can just look at the community. Scheme is a pure language with a very fragmented community. Nevertheless, I believe SchemeSpheres can be a useful project for a number of people and here I bring some new modules that might be useful if you are building software in Scheme.

In this post, I’ll summarize the different approaches to object systems that SchemeSpheres offers currently, hoping it can be useful or inspiring to you as a user of this or another Scheme platform. The code is (mostly) R5RS, so the greatest part of it can be used directly. The exception is the type-class system, which requires define-macro (or a low-level unhygienic macro system, like rsc-macro-transformer or syntax-case).

This is a blog post version of the guide Object Systems in SchemeSpheres, which is lengthier and more detailed (with examples and all).

SRFI-9 Records

Summary of the object system

This is a very simple object system: objects are just records, like C’s structs. They hold only data, and do not encapsulate behavior. Procedures designed to work with an instance of any record type can verify dynamically the right type through a predicate. This will signal to the procedure the possibility of calling the appropriate accessors/mutators associated to the record’s fields. Of course, a procedure could choose to directly call any of these functions, but if the procedure is passed an argument of the wrong type at runtime, a type exception will occur.

The key points of this system are:

  • The names of the record type, predicate, constructor and accessors/mutators are all listed explicitly in the source.
  • It doesn’t support inheritance.
  • Native in the Gambit compiler: no overhead.
  • The type and associated procedures can be described statically (with macros) given the right Scheme support. Gambit does support this, but through the define-type extension (please refer to Object Systems in SchemeSpheres).

Syntax and procedures

<record-type-definition> = (define-record-type
                            <type-name>
                            [ <constructor> <predicate> ]
                            <field> ...)

<constructor> = <constructor-name>
              | (<constructor-name> <field-tag> ...)

<predicate> = identifier

<field> = <field-tag>
        | (<field-tag>)
        | (<field-tag> <accessor-name>)
        | (<field-tag> <accessor-name> <modifier-name>)

<field-tag> = identifier
<*-name> = identifier

An instance of define-record-type is equivalent to the following definitions:

  • <type name> is bound to a representation of the record type itself. Operations on record types, such as defining print methods, reflection, etc. are left to other SRFIs.
  • <constructor name> is bound to a procedure that takes as many arguments as there are <field tag>s in the (<constructor name> …) subform and returns a new record. Fields whose tags are listed with <constructor name> have the corresponding argument as their initial value. The initial values of all other fields are unspecified.
  • <predicate name> is a predicate that returns #T when given a value returned by <constructor name> and #f for everything else.
  • Each <accessor name> is a procedure that takes a record of type <type name> and returns the current value of the corresponding field. It is an error to pass an accessor a value which is not a record of the appropriate type.
  • Each <modifier name> is a procedure that takes a record of type <type name> and a value which becomes the new value of the corresponding field; an unspecified value is returned. It is an error to pass a modifier a first argument which is not a record of the appropriate type.

SRFI-99 Records

Summary of the object system

This system is implemented as a module in SchemeSpheres (object: record), and uses the low-level facilities of Gambit for defining record types. The reference implementation will give you a portable code for implementing the SRFI.

These are the key features that bring to the native Gambit systems:

  • Adds data inheritance and inspection to SRFI-9 functionality.
  • High flexibility: you can manually generate each procedure and customize the methods.
  • Uses the native functionality to create the structures adding minimal overhead.
  • It is defined in three layers: procedural (generation of: constructor, predicate, accessor, mutator), inspection (query structures’ internals), syntactic (syntactic sugar for structure definition).

Most of the work for bringing SRFI-99 to Gambit has been done by Arthur T Smyles.

Syntax and procedures

This is the syntax brought by the syntactic layer, but is not necessary to use it (you can generate the procedures manually via the generators).

<record-type-definition> = (define-record-type <type-spec>
                            <constructor-spec>
                            <predicate-spec>
                            <field-spec> ...)

<type-spec> = <type-name>
            | (<type-name> <parent>)

<constructor-spec> = #f
                   | #t
                   | <constructor-name>
                   | (<constructor-name> <field-name> ...)

<predicate-spec> = #f
                 | #t
                 | <predicate-name>

<field-spec> = <field-name>
             | (<field-name>)
             | (<field-name> <accessor-name>)
             | (<field-name> <accessor-name> <mutator-name>)

<parent> = <expression>

<*-name> = identifier

These are the procedures in the procedural layer (rtd stands for record type descriptor):

(make-rtd <name> <fieldspecs> [<parent>])
(rtd? <obj>)
(rtd-constructor <rtd> [<fieldspecs>])
(rtd-predicate <rtd>)
(rtd-accessor <rtd> <field>)
(rtd-mutator <rtd> <field>)
(rtd-deconstructor <rtd> <field>) ; extension function, will return all fields as values

Finally, these are the procedures in the inspection layer:

(record? <obj>)
(record-rtd <record>)
(rtd-name <rtd>)
(rtd-parent <rtd>)
(rtd-field-names <rtd>)
(rtd-all-field-names <rtd>)
(rtd-field-mutable? <rtd> <field>)

;; extensions of the srfi-99
(rtd-uid <rtd>)
(rtd-sealed? <rtd>)
(rtd-opaque? <rtd>)
(rtd-field-flag-printable? <flags>)
(rtd-field-flag-mutable? <flags>)
(rtd-field-flag-equality? <flags>)
(rtd-field-flag-init? <flags>)

Type-classes

Summary of the object system

In essence, type-classes define a set of functions. Then, using this “interface”, other functions can be defined (called type-class polymorphic functions), similar to multimethods. Type-classes are parameterized types, acting like “types of types” in a sense: they allow you to describe the constraints a type should obey (type-class constraints), and any instance of the typeclass (a type) must conform to them. In other words, type-classes correspond to the set of types which have certain operations defined for them, and type class polymorphic functions work only for types which are instances of the type class(es) in question.

Type-classes can be used with records. They are similar to interfaces in OOP. Both define a set of types which implement a specified list of operations. However, type classes are more general for the following important reasons:

  • Generally in OOP, when a class is defined, any interfaces it implements must be declared. Type-class instances, on the other hand, are declared separately from the declaration of the corresponding types, and can even be put in a separate module.
  • The types that can be specified for type class methods are more general and flexible than the signatures that can be given for interface methods, especially when multi-parameter type-classes enter the picture.

While the previous object systems are focused on defining structures made of compound data (records), type-classes are focused on the functions defined for these structures. However, both type-classes and records can be combined to model more traditional OOP and extend its capabilities through the parameterized types.

The type-classes module can be summarized as follows:

  • The model is inspired by Haskell’s type-classes.
  • It introduces first-class types (the type-class instances), which brings powerful parametric types through type-class constraints.
  • It’s a way of creating generic functions (type-class polymorphic functions), defined for different types of the same type-class. You don’t need a function with a different name for each type. Furthermore, how this function should work for each type doesn’t need to be described in one place (as you could achieve with case-lambda). New data types can be added later without modifying previous code.
  • It offers a level of abstraction superior to CLOS-style generic functions and OOP in certain respects.
  • It’s compatible with record types.

The layer of type-classes adds an indirection to function calls: all functions are wrapped by extra lambdas.

Syntax and procedures

The following syntax is proposed by to André Van Tonder, who also implemented the syntax code in define-macro form.

(define-class <field-form> ...)

(define=> (<procedure-name> <class-form> ...) . body)

(lambda=> (<class-form> ...) . body)

(with (<instance-form> ...) . body)

(import-instance <instance-form> ...)

<field-form> = field-label
             | (<superclass-name> field-label)

<class-form> = <class-name>
             | (<class-name> <prefix-symbol>)

<instance-form> = (<class-name> <instance-expr>)
                | (<class-name> <instance-expr> <prefix-symbol>)

Prototypes (object: prototype)

Summary of the object system

Prototypes are a different approach to object modeling, based on the Self/JavaScript model. It can be used instead of records and type-classes. In this approach, behaviour reuse (inheritance) is performed cloning existing objects that serve as prototypes. The language feature that supports prototype-based programming is called delegation. This is the process in which the right function is selected to be dispatched for a given message passed to an object.

Objects in prototype-based programming can encapsulate both data and functionality, and serve as prototypes for other objects. The runtime of the system takes care of dispatching the correct method or finding the right piece of data simply by following a series of delegation pointers (from object to its prototype) until a match is found.

In a prototype-based object system, an object is just a set of slots. A slot has a name and a value, or a handler procedure which reacts on messages associated with the slot. Some slots are special, so-called parent slots, whose use will become apparent shortly.

Objects receive messages, consisting of a selector (a symbol), and zero or more arguments. When an object receives a message, the object searches for a slot whose name is equal (tested with eq?) to the message selector. When it finds such a slot, it invokes the slot’s handler, or returns the slot’s value, as appropriate. When the slot is not in the object, all objects in parent slots are queried for that slot. That is the process mentioned earlier called delegation.

An object is created by cloning an existing object. The new object is empty except for a single parent slot, which points to the cloned object. This way, the new object behaves exactly like the old one until its particular behaviour or data are changed by the programmer.

The main features of this system are:

  • It’s a system that seems more familiar to OO programmers: everything is an object, and slots can hold data or procedures. It’s inspired by Self and JavaScript prototypes.
  • It’s a very flexible and a fully dynamic system, but at the same time extremely lightweight.
  • On the other hand, delegation introduces indirection and some runtime checks when compared with plain records, thus bringing and inevitable performance hit.
  • Uses Self-like delegation semantics (build prototype, class, multiple-inheritance, whatever OO style you want)
  • Minimal syntax, minimal mechanism.
  • Naming is not required and it uses no global tables.

The prototypes system is a port of the project called TinyTalk, by Kenneth A Dicke.

Syntax and procedures

($ <message> <object> <arguments>) -- send a message to an object
(<< <object> <message> <arguments>) -- alternate syntax

(object (<field-spec> ...) <method-spec> ...) -- produces an object instance
(prototype-object? thing) -- universal predicate
(define-predicate <pred?>) -- defines a universal predicate
(string<< thing) -- returns a string describing thing


($ field-names <obj>) -- names of setter/getter field value access
($ shallow-clone <obj>) -- shallow-clone object (does *not* clone delegate(s))
($ deep-clone <obj>) -- deep-clone object (does clone delegate(s))
($ add-method! <obj> <name-sym> <proc>)
($ remove-method! <obj> <name-sym>)
($ methods-alist <obj>) -- all (name . method) pairs
($ lookup <obj>) -- symbol-> method or #f [single level (no delegate)]

($ ->string <obj>) -- descriptive string (should be implemented by user)

Did you find it useful? Please share!

Comments