#lang racket/base ;;; @Package Protobj ;;; @Subtitle Prototype-Delegation Object Model in Scheme ;;; @HomePage http://www.neilvandyke.org/protobj/ ;;; @Author Neil Van Dyke ;;; @Version 0.4 ;;; @Date 2011-11-08 ;;; @PLaneT neil/protobj:1:2 ;; $Id: protobj.rkt,v 1.70 2011/03/04 07:52:42 neilpair Exp $ ;;; @legal ;;; Copyright @copyright{} 2005--2011 Neil Van Dyke. This program is Free ;;; Software; you can redistribute it and/or modify it under the terms of the ;;; GNU Lesser General Public License as published by the Free Software ;;; Foundation; either version 3 of the License (LGPL 3), or (at your option) ;;; any later version. This program is distributed in the hope that it will be ;;; useful, but without any warranty; without even the implied warranty of ;;; merchantability or fitness for a particular purpose. See ;;; @indicateurl{http://www.gnu.org/licenses/} for details. For other licenses ;;; and consulting, please contact the author. ;;; @end legal (require scheme/mpair srfi/9) ;;; @section Introduction ;;; Protobj is a Scheme library that implements a simple prototype-delegation ;;; object model, somewhat similar to that of ;;; @uref{http://research.sun.com/self/papers/self-power.html, Self}, and also ;;; related to those of SLIB @code{object} and OScheme. Protobj was written ;;; mainly as a @code{syntax-rules} learning exercise, but also because people ;;; ask about prototype object models for Scheme from time to time. Like most ;;; object systems, Protobj should be regarded as an amusement. The Protobj ;;; library defines both a verbose set of procedures, and terse special syntax. ;;; ;;; Protobj is based on objects with named slots that can contain arbitrary ;;; values. Object have immediate slots, and single parent objects from which ;;; additional slots are inherited. When setting in a child object a slot ;;; inherited from the parent, a new immediate slot is created in the child so ;;; that the parent is unaffected and the slot is no longer inherited. ;;; ;;; Methods are simply closures stored in slots. When a method is applied, the ;;; first term of the closure is the receiver object. Unlike Self, getting the ;;; contents of the slot is distinguished from invoking a method contained in ;;; the slot. This distinction was made due to the way first-class closures ;;; are often used in Scheme. ;;; ;;; An object is cloned by invoking the @code{clone} method. The default root ;;; object's @code{clone} method creates a new child object without any ;;; immediate slots, rather than copying any slots. This behavior can be ;;; overridden to always copy certain slots, to copy immediate slots, or to ;;; copy all inherited slots. An overriding @code{clone} method can be ;;; implemented to apply its parent's @code{clone} method to itself and then ;;; set certain slots in the new child appropriately. ;;; ;;; Protobj requires R5RS, SRFI-9, SRFI-23, and SRFI-39. ;;; @section Tour ;;; The following is a quick tour of Protobj using the terse special syntax. ;;; ;;; @itemize ;;; ;;; @item ;;; Bind @code{a} to the new object that is created by cloning the default root ;;; object (@code{%} is special syntax for invoking the @code{clone} method): ;;; @lisp ;;; (define a (%)) ;;; @end lisp ;;; ;;; @item ;;; Verify that @code{a} is an object and that @code{a}'s parent is the default ;;; root object: ;;; @lisp ;;; (object? a) @result{} #t ;;; (eq? (^ a) (current-root-object)) @result{} #t ;;; @end lisp ;;; ;;; @item ;;; Add to @code{a} a slot named @code{x} with value @code{1}: ;;; @lisp ;;; (! a x 1) ;;; @end lisp ;;; ;;; @item ;;; Get @code{a}'s slot @code{x}'s value: ;;; @lisp ;;; (? a x) @result{} 1 ;;; @end lisp ;;; ;;; @item ;;; Bind @code{b} to a clone of @code{a}: ;;; @lisp ;;; (define b (% a)) ;;; @end lisp ;;; ;;; @item ;;; Get @code{b}'s slot @code{x}'s value, which is inherited from @code{a}: ;;; @lisp ;;; (? b x) @result{} 1 ;;; @end lisp ;;; ;;; @item ;;; Set @code{a}'s slot @code{x}'s value to @code{42}, and observe that ;;; @code{b} inherits the new value: ;;; @lisp ;;; (! a x 42) ;;; (? a x) @result{} 42 ;;; (? b x) @result{} 42 ;;; @end lisp ;;; ;;; @item ;;; Set @code{b}'s slot @code{x}'s value to @code{69}, and observe that @var{a} ;;; retains its own @code{x} value although @var{b}'s @code{x} value has been ;;; changed: ;;; @lisp ;;; (! b x 69) ;;; (? a x) @result{} 42 ;;; (? b x) @result{} 69 ;;; @end lisp ;;; ;;; @item ;;; Add to @code{a} an @code{xplus} slot containing a closure that implements a ;;; method of the object: ;;; @lisp ;;; (! a xplus (lambda ($ n) (+ (? $ x) n))) ;;; @end lisp ;;; ;;; @item ;;; Apply the method to the @code{a} and @code{b} objects (@code{b} inherits ;;; any new slots added to @code{a}): ;;; @lisp ;;; (@@ a xplus 7) @result{} 49 ;;; (@@ b xplus 7) @result{} 76 ;;; @end lisp ;;; ;;; @item ;;; Observe the shorthand syntax for applying methods to an object multiple ;;; times, with the syntax having the value of the lastmost application: ;;; @lisp ;;; (@@ a (xplus 1000) (xplus 7)) @result{} 49 ;;; @end lisp ;;; ;;; @item ;;; Bind to @var{c} an object that clones @var{a} and adds slot @var{y} with ;;; value @code{101}: ;;; @lisp ;;; (define c (% a (y 101))) ;;; @end lisp ;;; ;;; @item ;;; Get the values of both the @code{x} and @code{y} slots of @code{c}: ;;; @lisp ;;; (? c x y) @result{} 42 101 ;;; @end lisp ;;; ;;; @item ;;; Finally, bind @code{d} to a clone of @code{a} that overrides @code{a}'s ;;; @code{x} slot: ;;; @lisp ;;; (define d (% a (x 1) (y 2) (z 3))) ;;; (? d x y z) @result{} 1 2 3 ;;; @end lisp ;;; ;;; @end itemize ;;; @section Basic Interface ;;; The basic interface of Protobj is a set of procedures. (define-record-type object (%protobj:make-object parent slots) object? (parent object-parent %protobj:set-parent!) (slots %protobj:slots %protobj:set-slots!)) (define (%protobj:slots-assq slot-symbol slots) ;; Note: Sadly, we are no longer using assq for lookup, so it's slower. (let loop ((slots slots)) (cond ((null? slots) #f) ((eq? slot-symbol (mcar (car slots))) (car slots)) (else (loop (cdr slots)))))) (define (%protobj:find-slot obj slot-symbol proc noslot-thunk) (let loop ((o obj)) (cond ((%protobj:slots-assq slot-symbol (%protobj:slots o)) => proc) (else (cond ((object-parent o) => loop) (else (noslot-thunk))))))) ;;; @defproc object? x ;;; ;;; Predicate for whether or not @var{x} is a Protobj object. ;; see define-record-type ;;; @defproc object-parent obj ;;; ;;; Yields the parent object of object @var{obj}. ;; see define-record-type ;; TODO: Expose a "set-object-parent!"? ;;; @defproc object-set! obj slot-symbol val ;;; ;;; Sets the slot identified by symbol @var{slot-symbol} in object @var{obj} to ;;; value @code{val}. (define (object-set! obj slot-symbol val) (let ((slots (%protobj:slots obj))) (cond ((%protobj:slots-assq slot-symbol slots) => (lambda (slot) (set-mcdr! slot val))) (else (%protobj:set-slots! obj (cons (mcons slot-symbol val) slots)))))) ;;; @defproc object-get obj slot-symbol ;;; ;;; Yields the value of slot named by symbol @var{slot-symbol} in object ;;; @var{obj} (immediate or inherited). If no slot of that name exists, an ;;; error is signaled. (define (object-get obj slot-symbol) (%protobj:find-slot obj slot-symbol mcdr (lambda () (error "Object has no such slot:" obj slot-symbol)))) ;; (define (object-get/procs obj slot-symbol proc noslot-thunk) ;; (%protobj:find-slot obj ;; slot-symbol ;; (lambda (slot) (proc (cdr slot))) ;; noslot-thunk)) ;;; @defproc object-get obj slot-symbol noslot-thunk ;;; ;;; Yields the value of slot named by symbol @var{slot-symbol} in object ;;; @var{obj} (immediate or inherited), if any such slot exists. If no slot of ;;; that name exists, then yields the value of applying closure ;;; @var{noslot-thunk}. (define (object-get/noslot-thunk obj slot-symbol noslot-thunk) (%protobj:find-slot obj slot-symbol mcdr noslot-thunk)) ;;; @defproc object-apply obj slot-symbol arg ... ;;; ;;; Applies the method (closure) in the slot named by @var{slot-symbol} of ;;; object @var{obj}. The first term of the method is @var{obj}, and one or ;;; more @var{arg} are the remaining terms. If no such slot exists, an error ;;; is signaled. (define (object-apply obj slot-symbol . args) (apply (object-get obj slot-symbol) obj args)) ;;; @defproc object-apply/noslot-thunk obj noslot-thunk slot-symbol arg ... ;;; ;;; Like @code{object-apply}, except that, if the slot does not exist, instead ;;; of signalling an error, the value is the result of applying ;;; @var{noslot-thunk}. (define (object-apply/noslot-thunk obj slot-symbol noslot-thunk . args) (%protobj:find-slot obj slot-symbol (lambda (slot) (apply (mcdr slot) obj args)) noslot-thunk)) ;; TODO: Implement "object-apply/try", which calls a thunk (or is a no-op) if ;; no slot can be found. Maybe special syntax for doing this apply/try to a ;; parent. One of the things this might be most useful for is in a "clone" ;; method, to invoke any parent "clone" method within additional behavior. ;;; @defproc object-raw-clone/no-slots-copy obj ;;; @defprocx object-raw-clone/copy-immed-slots obj ;;; @defprocx object-raw-clone/copy-all-slots obj ;;; ;;; These procedures implement different ways of cloning an object, and are ;;; generally bound as @code{clone} methods in root objects. ;;; @code{/no-slots-copy} does not copy any slots, @code{/copy-immed-slots} ;;; copes immediate slots, and @code{/copy-all-slots} copies all slots ;;; including inherited ones. (define (object-raw-clone/no-slots-copy obj) (%protobj:make-object obj '())) (define (object-raw-clone/copy-immed-slots obj) (%protobj:make-object obj (map (lambda (pair) (mcons (mcar pair) (mcdr pair))) (%protobj:slots obj)))) (define (object-raw-clone/copy-all-slots obj) ;; Note: We could save a few "(%protobj:slots-assq X '())" calls by copying ;; the immediate slots first. (let loop-objs ((o obj) (seen '())) (if o (let loop-slots ((slots (%protobj:slots o)) (result seen)) (if (null? slots) (loop-objs (object-parent o) result) (loop-slots (cdr slots) (let ((name (mcar (car slots)))) (if (%protobj:slots-assq name seen) result (cons (mcons name (mcdr (car slots))) result)))))) (%protobj:make-object obj seen)))) ;; (define (object-clone obj) ;; (object-apply obj 'clone)) ;;; @defparam current-root-object ;;; ;;; Parameter for the default root object. The initial value is a root object ;;; that has @code{object-raw-clone/no-slots-copy} in its @code{clone} slot. ;; TODO: Make this a parameter, or lose it altogether. (define current-root-object (make-parameter (%protobj:make-object #f (list (mcons 'clone object-raw-clone/no-slots-copy))))) ;;; @section Terse Syntax ;;; Since Protobj's raison d'etre was to play with syntax, here it is. Note ;;; that slot names are never quoted. ;;; @defsyntax ^ obj ;;; ;;; Parent of @var{obj}. (define-syntax ^ (syntax-rules () ((_ OBJ) (object-parent OBJ)))) ;;; @defsyntax ! obj slot val ;;; @defsyntaxx ! obj (slot val) ... ;;; ;;; Sets object @var{obj}'s slot @var{slot}'s value to @var{val}. In the ;;; second form of this syntax, multiple slots of @var{obj} may be set at once, ;;; and are set in the order given. (define-syntax ! (syntax-rules () ((_ OBJ (S0 V0) (S1 V1) ...) (let ((temp OBJ)) (! temp S0 V0) (! temp S1 V1) ...)) ((_ OBJ S V) (object-set! OBJ (quote S) V)))) ;;; @defsyntax ? obj slot ... ;;; ;;; Yields the values of the given @var{slot}s of @var{obj}. If more than one ;;; @var{slot} is given, a multiple-value return is used. (define-syntax ? (syntax-rules () ((_ OBJ S) (object-get OBJ (quote S))) ((_ OBJ S0 ...) (let ((temp OBJ)) (values (? temp S0) ...))))) ;;; @defsyntax @@ obj slot arg ... ;;; @defsyntaxx @@ obj (slot arg ... ) ... ;;; ;;; Applies @var{obj}'s @var{slot} method, with @var{obj} as the first term and ;;; @var{arg}s as the remaining terms. In the second form of this syntax, ;;; multiple methods may be applied, and the value is the value of the last ;;; method application. (define-syntax %protobj:apply* (syntax-rules () ((_ (X0 X1 ...) S A0 ...) (let ((temp (X0 X1 ...))) (%protobj:apply* temp S A0 ...))) ((_ OVAR S A0 ...) ((object-get OVAR (quote S)) OVAR A0 ...)))) (define-syntax @ (syntax-rules () ((_ OBJ (S0 A0 ...) (S1 A1 ...) ...) (let ((temp OBJ)) (%protobj:apply* temp S0 A0 ...) (%protobj:apply* temp S1 A1 ...) ...)) ((_ OBJ S A ...) (%protobj:apply* OBJ S A ...)))) ;;; @defsyntax % [ obj (slot val) ... ] ;;; ;;; Clones object @var{obj}, binding any given @var{slot}s to respective given ;;; @var{val}s. (define-syntax % (syntax-rules () ((_) (% (current-root-object))) ((_ OBJ) (@ OBJ clone)) ((_ OBJ (S0 V0) (S1 V1) ...) (let ((temp (% OBJ))) (! temp S0 V0) (! temp S1 V1) ... temp)))) ;;; @unnumberedsec History ;;; @table @asis ;;; ;;; @item Version 0.4 --- 2011-11-08 --- PLaneT @code{(1 2)} ;;; Fixed @code{object?} not being exported. (Thanks to Shviller for reporting.) ;;; ;;; @item Version 0.3 --- 2009-03-03 --- PLaneT @code{(1 1)} ;;; License is now LGPL 3. Converted to authors new Scheme administration ;;; system. Changed slot lists and slot pairs to be explicitly mutable, for ;;; PLT 4.x. ;;; ;;; @item Version 0.2 --- 2005-06-19 -- PLaneT @code{(1 0)} ;;; Fixed bug in @code{%protobj:apply*} (thanks to Benedikt Rosenau for ;;; reporting). Changed @code{$} syntax to @code{?}, so that @code{$} could be ;;; used for ``self'' in methods. Documentation changes. ;;; ;;; @item Version 0.1 --- 2005-01-05 ;;; Initial release. ;;; ;;; @end table (provide ! % ? @ ^ current-root-object object? object-apply object-apply/noslot-thunk object-get object-get/noslot-thunk object-raw-clone/copy-all-slots object-raw-clone/copy-immed-slots object-raw-clone/no-slots-copy object-set!)