[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.