[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Programmer-defined data types



    Date: Thu, 24 Aug 89 11:55:21 edt
    From: Chris Hanson <cph@zurich.ai.mit.edu>

       Date: Mon, 21 Aug 89 11:34 PDT
       From: bawden.pa@xerox.com
       Line-Fold: no

	   Date: Fri, 18 Aug 89 18:36:26 PDT
	   From: Pavel.pa@Xerox.COM

	   (RECORD-CONSTRUCTOR rtd)

	   Returns a procedure for constructing new members of the type represented by
	   rtd.  The returned procedure accepts exactly as many arguments as there
	   were slot-names in the call to MAKE-RECORD-TYPE that created the type
	   represented by rtd; these are used, in order, as the initial values of
	   those slots in a new record, which is returned by the constructor
	   procedure.

       I prefer the alternative that someone (RRJ?) made the last time we
       discussed this:

	 (RECORD-CONSTRUCTOR rtd slot-names)

	 Where slot-names is a subset of the slot-names given to MAKE-RECORD-TYPE.
	 The returned procedure accepts exactly as many arguments as there are
	 slot-names.  It creates a new record and initializes the specified slots.

    I agree with this wholeheartedly.  I think this form of the
    constructor-constructor is significantly more useful. ...

I think you might want to allow a way to initialize the other arguments.
There are several ways you might want to do this, depending on how many
cases you want to handle and how convenient you want them to be:

 - If you want to say initial values can't depend on one another, an
   alist suffices. e.g.,

    (DEFINE FOO (MAKE-RECORD-TYPE 'FOO '(A B C D)))
    (DEFINE MAKE-FOO (RECORD-CONSTRUCTOR FOO '(A B C) '((D 0))))

 - If you want to say initial values can depend on one another, but
   beyond that, there are no ongoing constraints...

    (DEFINE PERSON (MAKE-RECORD-TYPE 'PERSON '(HAIR-COLOR EYE-COLOR)))

    (DEFINE     HAIR-COLOR (RECORD-ACCESSOR PERSON 'HAIR-COLOR))
    (DEFINE SET-HAIR-COLOR (RECORD-UPDATER  PERSON 'HAIR-COLOR))

    (DEFINE     EYE-COLOR (RECORD-ACCESSOR PERSON 'EYE-COLOR))
    (DEFINE SET-EYE-COLOR (RECORD-UPDATER  PERSON 'EYE-COLOR))

    (DEFINE MAKE-PERSON
	    (RECORD-CONSTRUCTOR 
	      PERSON '(HAIR-COLOR)
	      (LAMBDA (PERSON)
		(IF (MEMQ (HAIR-COLOR PERSON) '(BROWN BLACK))
		    (SET-EYE-COLOR PERSON 'BROWN)
		    (SET-EYE-COLOR PERSON 'BLUE)))))

 - If you want to say initial values can depend on one another on
   an ongoing basis, you have to extend the formalism further somehow.
   To explain this, I'll show you a buggy example and you can ponder
   how to fix it...

    (DEFINE POSITION
	    (MAKE-RECORD-TYPE 'POSITION '(X Y Z CACHED-DISTANCE-FROM-ORIGIN)))

    (DEFINE     X-POSITION (RECORD-ACCESSOR POSITION 'X))
    (DEFINE SET-X-POSITION (RECORD-UPDATER  POSITION 'X))

    (DEFINE     Y-POSITION (RECORD-ACCESSOR POSITION 'Y))
    (DEFINE SET-Y-POSITION (RECORD-UPDATER  POSITION 'Y))

    (DEFINE     Z-POSITION (RECORD-ACCESSOR POSITION 'Z))
    (DEFINE SET-Z-POSITION (RECORD-UPDATER  POSITION 'Z))

    (DEFINE DISTANCE-FROM-ORIGIN
	    (RECORD-ACCESSOR POSITION 'CACHED-DISTANCE-FROM-ORIGIN))
    (DEFINE SET-DISTANCE-FROM-ORIGIN
	    (RECORD-UPDATER  POSITION 'CACHED-DISTANCE-FROM-ORIGIN))

    (DEFINE (UPDATE-DISTANCE-FROM-ORIGIN RECORD)
      (SET-DISTANCE-FROM-ORIGIN
	RECORD (SQRT (EXPT (X-POSITION RECORD) 2)
		     (EXPT (Y-POSITION RECORD) 2)
		     (EXPT (Z-POSITION RECORD) 2))))
    (DEFINE MAKE-POSITION
	    (RECORD-CONSTRUCTOR POSITION '(X Y Z)
				UPDATE-DISTANCE-FROM-ORIGIN))
						
    The problem here is that every time you want to update X-, Y-, or
    Z-POSITION, you will have to update CACHED-DISTANCE-FROM-ORIGIN and
    there's no way to say that. A possible fix would be to generalize
    RECORD-UPDATER to take a similar argument, which was run to make any
    updates to `hidden' slots after the main assignment was done. If
    you did this, though, it would be necessary to generalize RECORD-UPDATER
    to take a list of slot names, rather than a single slot, so that the
    function didn't get run multiple times when you were trying to update
    several slots in parallel. e.g.,
     (RECORD-UPDATER '(X Y Z) UPDATE-DISTANCE-FROM-ORIGIN)
    would return you a function of four arguments, a record and new
    values for X, Y and Z.  Every time you called it, it would not only
    set the values but would run the function which updated the hidden
    slot.

    This use of a function to run after assignment has been done is very
    common in Flavors, by the way.  Of course, there it's done as part of
    a more general mechanism that no one is proposing here.  But this simple
    level of functionality offers an important hook that can be very powerful.
    And it doesn't cost anyone anything if they're not using it.

    Btw, if you change RECORD-UPDATER to require (or permit) a list, you
    might want to make an analogous change to RECORD-ACCESSOR.  In which
    case multiple values are the likely thing to want to return.
    [Now you can talk about this record stuff without feeling like you're
    interrupting the multiple values discussion. :-]
    If you do, then you're short a piece of language glue. Consider uses
    like:
         ;Apparent idiom for copying slots from RECORD1 to RECORD2
         (LAMBDA (RECORD1 RECORD2)
	   (WITH-VALUES
	     ((RECORD-ACESSOR POSITION '(X Y Z)) RECORD2) ;returns 3 values
	     (LAMBDA (NEW-X NEW-Y NEW-Z)
	       ((RECORD-UPDATER POSITION '(X Y Z)) RECORD1 NEW-X NEW-Y NEW-Z))))
    There is too much gratuitous glue needed to stick this together, which is
    an indictment of WITH-VALUES, if you ask me.  I'll show how
    COMPOSE-CONTINUATION could generalize to solve the problem, and then return
    to why WITH-VALUES isn't so good. Suppose we permit:
         (COMPOSE-CONTINUATION receiver arg1 ... argN generator)
    where arg1...argN were something to interpose in the values stream, permitting
    me to write:
	 (COMPOSE-CONTINUATION CONS 1 (LAMBDA () 2)) => (1 . 2)
    Getting back to the example, I could write the above idiom as:
	 (LAMBDA (RECORD1 RECORD2)
	   (COMPOSE-CONTINUATION
	     (RECORD-UPDATER POSITION '(X Y Z))
	     RECORD2
	     ((RECORD-ACESSOR POSITION '(X Y Z)) RECORD1)))
    Personally, i think this feels about as good as it could be. Certainly I see
    no wasted words, and the language glue permits the multi-valued functions to
    snap together as they need to--an important property of language glue.
    The proposed WITH-VALUES primitive doesn't seem to give me similar control.
    The only `syntactic room' for extra arguments in WITH-VALUES is after the first
    two arguments, so I would have to extend it as
	 (WITH-VALUES generator receiver arg1 arg2 ...)
    which would mean that the interposed argument, RECORD, would not be in the
    same order as it would appear in the values stream, writing the same idiom 
    from above as:
	 (LAMBDA (RECORD1 RECORD2)
	   (WITH-VALUES
	     ((RECORD-ACESSOR POSITION '(X Y Z)) RECORD1)
	     (RECORD-UPDATER POSITION '(X Y Z))
	     RECORD2))
    or it would mean that the updater needed to be changed to takes its RECORD
    argument last.  I don't like either of these solutions. And I do think the
    need to interpose new items into the data-flow path is a common one (that
    correctly led to the generalization of APPLY from a 2-argument function in
    Maclisp to a multi-arg function in Common Lisp), so I think it's something
    the designers of WITH-VALUES would do well to think carefully about.