A proposal for a SRFI of a prototype based object system.
Find a file
2025-02-03 10:00:53 +01:00
.gitignore In the beginning there was darkness 2025-02-03 10:00:53 +01:00
README.org In the beginning there was darkness 2025-02-03 10:00:53 +01:00
spec.org In the beginning there was darkness 2025-02-03 10:00:53 +01:00

Draft Pre-SRFI: Prototype Object System

Status

This SRFI is currently in conceptualization. This document is a rough draft.

Abstract

This SRFI proposes a "Self"-inspired prototype object system. Such an object system works by having prototype objects that are cloned repeatedly to modify, extend, and use them, and is interacted with by passing messages.

Issues

TBD

Rationale

Objects can be very useful for scenarios where an inheritance of functionality and a strong pairing between functionality and data is needed. Due to its simple yet versatile concept, a prototype-based object system is a good fit for Scheme to fill that role.

Specification

Object System - (srfi 999)

Objects

Objects are implemented as closures. To send a message to that object, the closure is applied to the message selector (i.e., the slot name), followed by a number of arguments. The return value(s) of the message are returned from this application.

Slots

The object system knows about three kinds of slots.

  • Value slots merely store a value which is returned when the corresponding message is received.
  • Parent slots are just like value slots, but have a special flag marking them as parents.
  • Method slots contain a procedure which is invoked for messages corresponding to this slot.

The procedure is called with at least two arguments, conventionally called self and resend. If the message received any arguments, they are also passed, after resend. Self is the object which received the messages, as opposed to the object where this message handler was found in. Resend is a procedure which can be used to resend the message to further parents, if the current method does not wish to handle the message. See "Inheritance" for more information about this.

A typical method handler could thus look like:

(lambda (self resend a b)
  (/ (+ a b)
     2))

Every slot, regardless of its kind, can be associated with a setter method when it is created. Setter methods receive a single argument, and replaces the value of the corresponding slot with this argument. Setter methods can be created automatically when a given slot is created, and are removed when the corresponding getter slot is removed (but not vice-versa). Because of this, they are sometimes not considered to be slots, even if they are. See Setters are Methods, for an example where this distinction is important.

Messages

Following the original concepts of object oriented programming, SRFI-999 uses messages to communicate with and between objects. To send a message to an object, the object has to be invoked with the target slot as first argument, and the slot's arguments as he further arguments. Sending a new x coordinate to a point object could look like this:

(pointobj 'set-x! 50)

Inheritance

When a slot for a message is not found in the current object, all its parent slots are queried recursively, i.e. parent objects which don't know the slot query their parents, etc.

If no parent knows the slot, the original message receiving object is sent a message-not-understood message. If more than one parent knows the slot, the original message receiving object is sent a ambiguous-message-send message. See Root Objects, for a documentation of those messages. By default, they signal an error.

Method slots can decide not to handle the message, but rather search the inheritance tree for other handlers. For this purpose, they are passed a procedure commonly called resend. See Slots, for an explanation of method slots.

It is important to understand the difference between sending a message to an object, and resending it to the object. When a message is just sent to an object, methods will get that object as the self argument. When a message is resent, self instead refers to the object it was originally sent to.

Interface

Since objects are created by sending a clone message to other objects, there has to be a kind of root object. The object system provides a procedure to create such root objects.

make-root-object - procedure

(make-root-object)

This creates a new root object from which other objects can be cloned. This object is independent of any other object, and thus creates a new inheritance tree.

*the-root-object* - class

This is the default root object. If not really intended otherwise, this should be used as the root of the object hierarchy in a program.

Root objects contain a number of slots by default.

clone - message

Return a clone of the message recipient. This creates a new object with a single slot, parent, which points to the object cloned from.

add-value-slot! getter [setter] value - message

Add a new value slot to the recipient. The value of the slot can be retrieved with the getter message. If a setter message is given, that message can be used to change the value of the slot.

add-method-slot! getter [setter] proc - message

Add a method to the recipient. Sending the object a getter message now invokes proc with the same arguments as the message, in addition to a self argument pointing to the current object and a resend procedure available to resend the message if the method does not want to handle it directly.

The setter message can later be used to change the procedure.

add-parent-slot! getter [setter] parent - message

Add a parent slot to the recipient. Parent slots are searched for slots not found directly in the object. The setter message, if given, can be used to later change the value of the parent slot.

delete-slot! name - message

Delete the slot named name from the receiving object. This also removes the setter corresponding to name, if any. Beware that the parents might contain the same slot, so a message send can still succeed even after a slot is deleted.

immediate-slot-list - message

This message returns a list of slots in this object. The elements of the lists are lists with three elements each:

  • getter-name
  • setter-name or #f
  • type, which can be one of the symbols value, method, or parent.

message-not-understood message args - message

This is received when the message message with arguments args to the object was not understood. The root object just signals an error.

ambiguous-message-send message args - message

This is received when the message message with arguments args to the object would have reached multiple parents. The root object just signals an error.

Syntactic Sugar (srfi 999 syntax)

define-method (obj message self resend . args) body ... - syntax

This is syntactic sugar for the often-used idiom to define a method slot, by sending a add-method-slot! message with a message name and a lambda form with self, resend and args formals, and a body. This shortens the following code:

  (obj 'add-method-slot!
       'average
       (lambda (self resend a b)
         (/ (+ a b) 2)))

to this:

  (define-method (obj 'average self resend a b)
    (/ (+ a b) 2))

define-object name (parent other-parents ...) slots ... - syntax

This is syntactic sugar for the typical actions of cloning an object from a parent object, and adding more slots.

other-parents is a list of (name object) lists, where each object is added as a parent slot named name.

slots is a list of slot specifications, either (getter value) or (getter setter value) for value slots, or ((name self resend args ...) body ...) for method slots.

This allows for shortening the following example:

  (define o (*the-root-object* 'clone))
  (o 'add-value-slot! 'constant 'set-constant! 5)
  (o 'add-method-slot! 'add
     (lambda (self resend summand)
       (+ summand (self 'constant))))

into this:

  (define-object o (*the-root-object*)
    (constant set-constant! 5)
    ((add self resend summand)
     (+ summand (self 'constant))))

Further Concepts and Thoughts

Private Messages

Message names don't have any required type. They are only compared using eq?. Because of this, any kind of Scheme object can be used as a message name. This means that it is possible to use a private Scheme value - for example, a freshly-allocated list - as a slot name. This can be used to keep slot names private, since it is not possible to create an object which is eq? to such an object except by receiving a reference to that object.

Sample Implementation

The sample implementation is written in Chicken Scheme, but no Chicken Scheme specific features have been used.

Authors

Original documentation for Prometheus: Jorgen Schaefer. SRFI specification based on the original documentation: Daniel Ziltener.

Acknowledgements

This specification is based on the implementation and documentation of the Prometheus object system by Jorgen Schaefer written for Scheme48.

Copyright

Copyright (C) Jorgen Schaefer, Daniel Ziltener (2025).

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 (including the next paragraph) 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.