Changeset 17956 in project


Ignore:
Timestamp:
04/28/10 08:40:56 (9 years ago)
Author:
felix winkelmann
Message:

integrated Dickey's article into wiki page for yasos

File:
1 edited

Legend:

Unmodified
Added
Removed
  • wiki/eggref/4/yasos

    r17955 r17956  
    2525(require-extension yasos)
    2626
    27 === Documentation
    28 
    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
     28
     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.
     34
     35==== The Classical Object Model
     36
     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
     47languages.
     48
     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.
     62
     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
     72`class'.
     73
     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.
     81
     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.
     87
     88
     89==== Object Based Message Passing
     90
     91
     92So how can this "message passing" be implemented with lexical
     93closures?  And what are these closure things anyway?
     94
     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."
     103
     104
     105An example may help here:
     106
     107  (define (CURRIED-ADD x) (lambda (y) (+ x y))
     108
     109  (define ADD8 (curried-add 8))
     110
     111  (add8 3)      -> 11
     112
     113
     114When add8 is applied to its argument, we are doing ((lambda (y) (+ x y)) 3)
     115
     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.
     119
     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.
     123
     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:
     127
     128
     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  ) )  )
     144
     145
     146
     147  (define p1 (make-point 132 75))
     148
     149  (define p2 (make-point 132 57))
     150
     151  ((p1 'x))             -> 132
     152
     153  ((p1 'set-x!) 5)      -> 5
     154
     155
     156We can even change the message passign style to function calling style:
     157
     158  (define (x obj) (obj 'x))
     159
     160  (define (set-x! obj new-val) ((obj 'set-x!) new-val))
     161
     162
     163  (set-x! p1 12)        -> 12
     164
     165  (x p1)                -> 12
     166
     167  (x p2)                -> 132  ;; p1 and p2 share code but have different local data
     168
     169
     170Using Scheme's lexical scoping, we can also define make-point as:
     171
     172  (define (MAKE-POINT the-x the-y)
     173
     174    (define (get-x) the-x)      ;; a "method"
     175
     176    (define (get-y) the-y)
     177
     178    (define (set-x! new-x)
     179       (set! the-x new-x)
     180       the-x)
     181
     182    (define (set-y! new-y)
     183       (set! the-y new-y)
     184       the-y)
     185
     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))))
     193
     194    self         ;; the return value of make-point is the dispatch function
     195  )
     196
     197
     198
     199==== Adding Inheritance
     200
     201
     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!
     207
     208
     209  (define (MAKE-POINT-3D a b the-z)
     210    (let ( (point (make-point a b)) )
     211
     212     (define (get-z) the-z)
     213
     214     (define (set-z! new-value)
     215        (set! the-z new-value)
     216        the-z)
     217
     218     (define (self message)
     219       (case message
     220           ((z)                 get-z)
     221           ((set-z!)    set-z!)
     222           (else (point message))))
     223
     224    self
     225  )
     226
     227  (define p3 (make-point-3d 12 34 217))
     228
     229  (x p3)                -> 12
     230
     231  (z p3)                -> 217
     232
     233  (set-x! p3 12)        -> 12
     234
     235  (set-x! p2 12)        -> 12
     236
     237  (set-z! p3 14)        -> 14
     238
     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.
     241
     242
     243==== What Is Wrong With The Above Picture ?
     244
     245
     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."
     252
     253  (define (POINT? obj) (and (procedure? obj) (obj 'point?)))
     254
     255  (point? list)         -> (point?)  ;; a list with the symbol 'point?
     256
     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.
     262
     263
     264==== One Set Of Solutions
     265
     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.
     270
     271In order to know what data objects are "instances", we have a
     272predicate, INSTANCE?, which takes a single argument and answers #t or
     273#f. 
     274
     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. 
     278
     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.
     284
     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:
     289
     290   (OBJECT-WITH-ANCESTORS ( (ancestor1 init1) ...)
     291     operation ...).
     292
     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.
     296
     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 ...).
     300
     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>!
     303
     304
     305==== Examples
     306
     307O.K., let's see how this fits together.  First, another look at
     308points.
     309
     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"
     324
     325Things to notice...
     326
     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
     332 
     333
     334Now lets look at a more interesting example, a simplified savings
     335account with history and passwords.
     336
     337''(See below for the example code)''
     338
     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))
     342
     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
     351
     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)
     356
     357  ;; Fred forgets
     358  (ssn fred 'bogus)     Bad password: bogus     ;; Fred gets another chance
     359
     360  ;; Sally forgets
     361  (ssn sally 'bogus)    CALL THE POLICE!!       ;; A more serious result..
     362
     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.
     368
     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.
     374
     375
     376==== Our Implementation
     377
     378Given the sophisticated behavior of our object system, the
     379implementation is surprisingly small.
     380
     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.
     386
     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.
     392
     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.
     400
     401
     402==== How This Compares To The Classical Model
     403
     404It is time to compare this implementation to the model given at the
     405beginning of this article.
     406
     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.
     413
     414The Scheme solution is also more general.  It keeps lexical scoping,
     415and one can freely mix OO with functional & imperative styles.
     416
     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.
     420
     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.
     432
     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).
     437
     438Remember, your programming language should be part of the solution, not
     439part of your problems.  Scheme for success!
     440
    34441
    35442=== Examples
Note: See TracChangeset for help on using the changeset viewer.