Changeset 17956 in project

04/28/10 08:40:56 (9 years ago)
felix winkelmann

integrated Dickey's article into wiki page for yasos

1 edited


  • wiki/eggref/4/yasos

    r17955 r17956  
    2525(require-extension yasos)
    27 === Documentation
    29  (define-operation (opname self arg ...) default-body)
    30  (define-predicate opname)
    31  (object ((name self arg ...) body) ...)
    32  (object-with-ancestors ((ancestor1 init1) ...) operation ...)
    33  (operate-as component operation self arg ...)
     27=== Scheming with  Objects
     29There is a saying--attributed to Norman Adams--that "Objects are a
     30poor man's closures." In this article we discuss what closures are and
     31how objects and closures are related, show code samples to make these
     32abstract ideas concrete, and implement a Scheme Object System which
     33solves the problems we uncover along the way.
     35==== The Classical Object Model
     37Before discussing object oriented programming in Scheme, it pays to
     38take a look at the classical model so that we have something to
     39compare with and in order to clarify some of the terminology.  One of
     40the problems that the OO movement created for itself was the use of
     41new terms to get away from older concepts and the confusion this has
     42caused.  So before going further I would like to give some of my own
     43definitions and a simple operational model.  The model is not strictly
     44correct as most compiled systems use numerous short cuts and special
     45optimization tricks, but it is close enough for most practical
     46purposes and has been used to implement OO programming in imperative
     49An object "instance" consists of local (encapsulated) state and a
     50reference to shared code which operates on its state.  The easy way to
     51think of this is as a C struct or Pascal record which has one field
     52reserved for a pointer to its shared code environment and other slots
     53for its instance variables.  Each procedure in this shared environment
     54is called a "method." A "class" is code which is can generate
     55instances (new records) by initializing their fields, including a
     56pointer to the instance's shared method environment.  The environment
     57just maps method names to their values (their code).  Each method is a
     58procedure which takes the record it is operating on as its first,
     59sometimes hidden, argument.  The first argument is called the
     60"reciever" and typically aliased to the name "self" within the
     61procedure's code.
     63In order to make code management easy, object oriented systems such as
     64Actor or Smalltalk wish to deal with code as objects and the way this
     65is done is by making each class an object instance as well.  In order
     66to manipulate the class's code, however a "meta-class" is typically
     67defined and in some cases a meta-meta...  Well, you get the idea.
     68Many people have spent a great deal of time in theories of how to
     69"ground" such systems without infinite recursion.  To confuse things
     70further, many object systems have an object named "object" and a class
     71object named "class"--so that the class of the "class" object is
     74By making every data object an instance of the OO system, uniformity
     75demands that numbers are added, e.g. 1 + 2 by "sending the message" +
     76to the object 1 with the argument 2.  This has the advantage that + is
     77polymorphic--it can be applied to any data object.  Unfortunately,
     78polymorphism also makes optimization hard in that the compiler can no
     79longer make assumptions about + and may not be able to do constant
     80folding or inlining.
     82The set of methods an object responds to is called a "protocol".
     83Another way of saying this is that the functions or operations that
     84are invokeable on an object make up its interface.  More than one
     85class of object may respond to the same protocol--i.e. many different
     86types of objects have the same operation names available.
     89==== Object Based Message Passing
     92So how can this "message passing" be implemented with lexical
     93closures?  And what are these closure things anyway?
     95References within a function to variables outside of the local
     96scope--free references--are resolved by looking them up in the
     97environment in which the function finds itself.  When a language is
     98lexically scoped, you see the shape of the environment when you
     99read--lex--the code.  In Scheme, when a function is created it
     100remembers the environment in which it was created.  Free names are
     101looked up in that environment, so the environment is said to be
     102"closed over" when the function is created.  Hence the term "closure."
     105An example may help here:
     107  (define (CURRIED-ADD x) (lambda (y) (+ x y))
     109  (define ADD8 (curried-add 8))
     111  (add8 3)      -> 11
     114When add8 is applied to its argument, we are doing ((lambda (y) (+ x y)) 3)
     116The function add8 remembers that X has the value 8.  It gets the value
     117Y when it is applied to 3.  It finds that + is the addition function.
     118So (add8 3) evaluates to 11.
     120(define ADD5 (curried-add 5)) makes a new function which shares the
     121curried-add code (lambda (y) (+ x y)), but remembers that in its
     122closed over environment, X has the value 5.
     124Now that we have a way to create data objects, closures, which share
     125code but have different data, we just need a "dispatching function" to
     126which we can pass the symbols we will use for messages:
     129  (define (MAKE-POINT the-x the-y)
     130    (lambda (message)
     131       (case message
     132          ((x)  (lambda () the-x)) ;; return a function which returns the answer
     133          ((y)  (lambda () the-y))
     134          ((set-x!)
     135               (lambda (new-value)
     136                       (set! the-x new-value)  ;; do the assignment
     137                        the-x))                ;; return the new value
     138          ((set-y!)
     139               (lambda (new-value)
     140                       (set! the-y new-value)
     141                        the-y))
     142         (else (error "POINT: Unknown message ->" message))
     143  ) )  )
     147  (define p1 (make-point 132 75))
     149  (define p2 (make-point 132 57))
     151  ((p1 'x))             -> 132
     153  ((p1 'set-x!) 5)      -> 5
     156We can even change the message passign style to function calling style:
     158  (define (x obj) (obj 'x))
     160  (define (set-x! obj new-val) ((obj 'set-x!) new-val))
     163  (set-x! p1 12)        -> 12
     165  (x p1)                -> 12
     167  (x p2)                -> 132  ;; p1 and p2 share code but have different local data
     170Using Scheme's lexical scoping, we can also define make-point as:
     172  (define (MAKE-POINT the-x the-y)
     174    (define (get-x) the-x)      ;; a "method"
     176    (define (get-y) the-y)
     178    (define (set-x! new-x)
     179       (set! the-x new-x)
     180       the-x)
     182    (define (set-y! new-y)
     183       (set! the-y new-y)
     184       the-y)
     186    (define (self message)
     187       (case message
     188          ((x)            get-x) ;; return the local function
     189          ((y)            get-y)
     190          ((set-x!) set-x!)
     191          ((set-y!) set-y!)
     192          (else (error "POINT: Unknown message ->" message))))
     194    self         ;; the return value of make-point is the dispatch function
     195  )
     199==== Adding Inheritance
     202"Inheritance" means that one object may be specialized by adding to
     203and/or shadowing another's behavior.  It is said that "object based"
     204programming together with inheritance is "object oriented" programming.
     205How can we add inheritance to the above picture?  By delegating to
     206another object!
     209  (define (MAKE-POINT-3D a b the-z)
     210    (let ( (point (make-point a b)) )
     212     (define (get-z) the-z)
     214     (define (set-z! new-value)
     215        (set! the-z new-value)
     216        the-z)
     218     (define (self message)
     219       (case message
     220           ((z)                 get-z)
     221           ((set-z!)    set-z!)
     222           (else (point message))))
     224    self
     225  )
     227  (define p3 (make-point-3d 12 34 217))
     229  (x p3)                -> 12
     231  (z p3)                -> 217
     233  (set-x! p3 12)        -> 12
     235  (set-x! p2 12)        -> 12
     237  (set-z! p3 14)        -> 14
     239Note that in this style, we are not required to have a single distinguished
     240base object, "object"--although we may do so if we wish.
     243==== What Is Wrong With The Above Picture ?
     246While the direct strategy above is perfectly adequate for OO
     247programming, there are a couple of rough spots.  For example, how can
     248we tell which functions are points and which are not?  We can define a
     249POINT?  predicate, but not all Scheme data objects will take a 'point?
     250message.  Most will generate error messages, but some will just "do
     251the wrong thing."
     253  (define (POINT? obj) (and (procedure? obj) (obj 'point?)))
     255  (point? list)         -> (point?)  ;; a list with the symbol 'point?
     257We want a system in which all objects participate and in which we can
     258mix styles.  Building dispatch functions is repetitive and can
     259certainly be automated--and let's throw in multiple inheritance while
     260we are at it.  Also, it is generally a good design principle to
     261separate interface from implementation, so we will.
     264==== One Set Of Solutions
     266The following is one of a large number of possible implementations.
     267Most Scheme programmers I know have written at least one object system
     268and some have written several.  Let's first look at the interface, then
     269how it is used, then how it was implemented.
     271In order to know what data objects are "instances", we have a
     272predicate, INSTANCE?, which takes a single argument and answers #t or
     275For each kind of object is also useful to have a predicate, so we
     276define a predicate maker: (DEFINE-PREDICATE <opname?>) which by default
     277answers #f. 
     279To define operations which operate on any data, we need a default
     280behavior for data objects which don't handle the operation:
     281(DEFINE-OPERATION (opname self arg ...) default-body).  If
     282we don't supply a default-behavior, the default default-behavior
     283is to generate an error.
     285We certainly need to return values which are instances of our object
     286system: (OBJECT operation... ), where an operation has the form:
     287((opname self arg ...) body).  There is also a LET-like form for
     288multiple inheritance:
     290   (OBJECT-WITH-ANCESTORS ( (ancestor1 init1) ...)
     291     operation ...).
     293In the case of multiple inherited operations with the same identity,
     294the operation used is the one found in the first ancestor in the
     295ancestor list.
     297And finally, there is the "send to super" problem, where we want to
     298operate as an ancestor, but maintain our own self identity {more on
     299this below}:  (OPERATE-AS component operation composite arg ...).
     301Note that in this system, code which creates instances is just code, so
     302there there is no need to define "classes" and no meta-<anything>!
     305==== Examples
     307O.K., let's see how this fits together.  First, another look at
     310  (define P2 (make-point 123 32131))
     311  (define P3 (make-point-3d 32 121 3232))
     312  (size "a string")     -> 8
     313  (size p2)             -> 2
     314  (size p3)             -> 3
     315  (point? p2)           -> #t
     316  (point? p3)           -> #t
     317  (point? "a string")   -> #f
     318  (x p2)                        -> 123
     319  (x p3)                        -> 32
     320  (x "a string")                -> ERROR: Operation not handled: x "a string"
     321  (print p2 #t)         #<point: 123 32131>
     322  (print p3 #t)         #<3D-point: 32 121 3232>
     323  (print "a string" #t)         "a string"
     325Things to notice...
     327* Interface is separate from implementation
     328* All objects participate
     329* Inheritance is simplified
     330* Print unifies treatment of objects--each decides how it is to be displayed
     331* Default behaviors are useful
     334Now lets look at a more interesting example, a simplified savings
     335account with history and passwords.
     337''(See below for the example code)''
     339  (define FRED  (make-person "Fred" 19 "573-19-4279" #xFadeCafe))
     340  (define SALLY
     341    (make-account "Sally" 26 "629-26-9742" #xFeedBabe 263 bank-password))
     343  (print fred #t)               #<Person: Fred age: 19>
     344  (print sally #t)      #<Bank-Customer Sally>
     345  (person? sally)               -> #t
     346  (bank-account? sally) -> #t
     347  (ssn fred  #xFadeCafe)        -> "573-19-4279"
     348  (ssn sally #xFeedBabe)        -> "629-26-9742"
     349  (add sally 130)       New balance: $393
     350  (add sally 55)                New balance: $448
     352  ; the bank can act in Sally's behalf
     353  (get-account-history sally bank-password)             --> (448 393 263)
     354  (withdraw sally 100 (get-pin sally bank-password))    New balance: $348
     355  (get-account-history sally bank-password)             --> (348 448 393 263)
     357  ;; Fred forgets
     358  (ssn fred 'bogus)     Bad password: bogus     ;; Fred gets another chance
     360  ;; Sally forgets
     361  (ssn sally 'bogus)    CALL THE POLICE!!       ;; A more serious result..
     363Now we see the reason we need OPERATE-AS.  The when the bank-account
     364object delegates the SSN operation to its ancestor, person, SELF is
     365bound to the bank-account object--not the person object.  This means
     366that while the code for SSN is inherited from person, the BAD-PASSWORD
     367operation of the bank-account is used.
     369This is an important behavior to have in an object system.  If there
     370were no OPERATE-AS, code would have to be duplicated in order to
     371implement the stricter form of BAD-PASSWORD.  With OPERATE-AS, we can
     372safely share code while keeping operations localized within the
     373inheritance hierarchy.
     376==== Our Implementation
     378Given the sophisticated behavior of our object system, the
     379implementation is surprisingly small.
     381Unlike some other languages, Scheme does not have a standard way of
     382defining opaque types.  In order to distinguish data objects which are
     383instances of our object system, we just uniquely tag a closure.  As we
     384are only introducing one new datatype it is not much work to hide this
     385by rewriting Scheme's WRITE and DISPLAY routines.
     387In order to allow lexical scoping of objects and operations, the
     388values of operations, rather than their names, are used in the
     389dispatch functions created for objects.  Those of you who have used
     390languages such as Smalltalk or Actor may have been bitten by the
     391inadvertant name collisions in the single, global environment.
     393Note that there are no global tables.  A general rule of thumb is that
     394for less than 100 elements, linear search beats hashing.  While we can
     395speed up operation dispatch by some simple caching, the general
     396performance for this system will be pretty good up through moderately
     397large systems.  Also, we can optimize the implementation with no
     398change to the interface.  If our systems start getting too slow, its
     399time to smarten the compiler.
     402==== How This Compares To The Classical Model
     404It is time to compare this implementation to the model given at the
     405beginning of this article.
     407One thing you may notice right away is the power of closures.  The
     408object system is small and simpler than the class model.  There are no
     409grounding problems.  No "Meta".  I find it interesting that
     410Whitewater's Actor 4.0 implements code sharing between classes (which
     411they call multiple inheritance) in an attempt to get more of the
     412benefits that closures provide directly.
     414The Scheme solution is also more general.  It keeps lexical scoping,
     415and one can freely mix OO with functional & imperative styles.
     417Programming Environment work still has to be done for code management
     418& debugging (e.g. doing an object inspector), but OO helps here just
     419as in other OO systems.
     421Separating the interface from the implementation is a better software
     422engineering solution than the classical model.  We can define our
     423"protocols" independently of their implementation.  This helps us hide
     424our implementation.  One might think that object oriented programming
     425in general would solve the problems here, but this has not been the
     426case because people still use inheritance to share code rather than
     427just to share abstractions.  An example of this is the complex
     428behavior of Smalltalk dictionaries because they inherit the
     429implementation of Sets.  While code sharing is a benefit of OO it is
     430still considered bad form when your code breaks because of a change in
     431the implementation of an inherited abstraction.
     433Finally, I would like to point out that one can implement other OO
     434models directly in Scheme, including smaller, simpler ones!  You can
     435also implement the classical model (e.g. see D. Friedman, M. Wand, &
     436C. Haynes: _Essentials of Programming Languages_, McGraw Hill, 1992).
     438Remember, your programming language should be part of the solution, not
     439part of your problems.  Scheme for success!
    35442=== Examples
Note: See TracChangeset for help on using the changeset viewer.