Cleanup Issue DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES

Forum
Cleanup
Category
CLARIFICATION, CHANGE
References
DEFSTRUCT; CLtL p. 309 Issue DEFSTRUCT-CONSTRUCTOR-KEY-MIXTURE (passed)

Problem Description

Must the symbols which name DEFSTRUCT slots be bound as lambda variables by the default keyword constructor function? Normally it would not matter, but if any of these symbols have been proclaimed SPECIAL it will affect the dynamic environment in which the slot init forms are evaluated.

There are three proposals, BOUND, NOT-BOUND, and VISIBLY-BOUND.

Background:

CLtL requires each default slot init form to be evaluated "in the lexical environment of the DEFSTRUCT form in which it appeared". This means that the obvious technique of supplying the init forms as the defaults for the keyword arguments in the lambda list of the constructor function is incorrect, unless care is taken to avoid shadowing any variable bindings of the symbols which correspond to those arguments.

For example, given (defstruct foo (a <a-init>) (b <b-init>) (c <c-init>))

Generating the constructor as (defun make-foo (&key (a <a-init>) (b <b-init>) (c <c-init>)) ...) may not evaluate <b-init> and <c-init> in the correct lexical environment as specified in CLtL. Proposal VISIBLY-BOUND would change the specification to make this the correct behavior.

One alternative is to wrap the init forms in closures named with gensyms: (flet ((#:g1 () <a-init>) (#:g2 () <b-init>) (#:g3 () <c-init>)) (defun make-foo (&key (a (#:g1)) (b (#:g2)) (c (#:g3))) ...)) Under proposal BOUND, this would be the correct way to implement the constructor function.

Another alternative is to make the lambda variables themselves gensyms: (defun make-foo (&key ((:a #:g4) <a-init>) ((:b #:g5) <b-init>) ((:c #:g6) <c-init>)) ...) Under proposal NOT-BOUND, this would be the correct way to implement the constructor function.

(Of course, it's possible that DEFSTRUCT could produce a simplified expansion in many cases by examining the init forms and/or lexical environment.)

Issue DEFSTRUCT-CONSTRUCTOR-KEY-MIXTURE implies that BOA constructors do bind the symbols which name slots as lambda variables, since these variables can be referenced in the init forms for subsequent arguments.

Proposal (BOUND)

Clarify that the symbols which name slots must be bound as lambda variables by the keyword constructor function, in the order in which the slots are specified in the DEFSTRUCT form. Variables for inherited slots are bound before variables for explitly specified slots, in the order in which they were specified in the definition of the inherited structure. Special bindings of these variables will be visible during the evaluation of the default init forms for subsequent slots. The slot default init forms are still evaluated in the lexical environment in which the DEFSTRUCT form itself appears.

Rationale

This appears to be closest to the intent of CLtL.

Proposal (NOT-BOUND)

Clarify that the symbols which name slots must *not* be bound as lambda variables by the keyword constructor function. The slot default init forms are evaluated in the lexical environment in which the DEFSTRUCT form itself appears and the dynamic environment in which the call to the constructor function appears.

Rationale

This avoids the overhead of creating and invoking closures to compute the default values of the slots for the default keyword constructor.

Proposal (VISIBLY-BOUND)

Clarify that the symbols which name slots must be bound as lambda variables by the keyword constructor function, in the order in which the slots are specified in the DEFSTRUCT form. Variables for inherited slots are bound before variables for explitly specified slots, in the order in which they were specified in the definition of the inherited structure. Special bindings of these variables will be visible during the evaluation of the default init forms for subsequent slots.

Remove the requirement that the slot default init forms be evaluated in the lexical environment in which the DEFSTRUCT form itself appears. Instead, require that they be evaluated in a lexical environment that contains bindings for the previous lambda variables of the constructor function. This applies to both the default keyword constructor function and BOA constructors.

Rationale

This alternative corresponds most closely to current practice. It avoids the overhead of creating and invoking closures to compute the default values of the slots for both the default keyword constructor and BOA constructors.

Example/Test Cases:

(defvar x 'global-x) (let ((y 'local-y)) (defstruct baz (x 'x-init) (y x) (z y))) (make-baz)

Under proposal BOUND, slot X is initialized to X-INIT slot Y is initialized to X-INIT (since the init form X is evaluated in the dynamic environment containing the binding to X-INIT) slot Z is initialized to LOCAL-Y (since the init form Y is evaluated in the lexical environment in which the DEFSTRUCT appears)

Under proposal NOT-BOUND, slot X is initialized to X-INIT slot Y is initialized to GLOBAL-X (since the constructor does not rebind the special variable X) slot Z is initialized to LOCAL-Y

Under proposal VISIBLY-BOUND, slot X is initialized to X-INIT slot Y is initialized to X-INIT (since the special binding of X made by the constructor is visible) slot Z is initialized to X-INIT (since the lexical binding of Y made by the constructor is visible)

Current Practice

Most implementations (including Lucid CL, HPCL-I, KCL, CMU Common Lisp) appear to implement proposal VISIBLY-BOUND even though it is in conflict with what is required by CLtL.

Utah Common Lisp currently implements proposal NOT-BOUND.

Cost to Implementors

For proposal BOUND, the cost of implementing the proposal correctly is fairly small. The cost of implementing it both correctly and efficiently is potentially much larger.

For proposal NOT-BOUND, the implementation cost is again fairly small, but it still requires essentially the same work as in proposal BOUND to handle BOA constructors correctly.

Proposal VISIBLY-BOUND has the least implementation cost, since this is what most implementations already do and is the least complicated of the alternatives.

Cost to Users

Adopting any of these proposals would improve the situation faced by users now.

Users may find proposal VISIBLY-BOUND to be marginally more useful than the other alternatives since it allows the values of slots to be referenced in the subsequent default init forms.

Benefits

An area of confusion in the language is removed.

Discussion

Loosemore doesn't care much about which of these alternatives we adopt, but thinks that leaving this unspecified would be a mistake.

Margolin says: By the way, I prefer this proposal [NOT-BOUND]. I think lexical environments should be captured (I think we've fixed everything so that init-forms are always evaluated in the appropriate lexical environment), but I don't like making the order of slots significant by allowing init forms to reference other slots. Order of slots is often constrained by other requirements (such as the :INCLUDE hierarchy, or using :TYPE to match a pre-existing structure), so they shouldn't have an effect on the semantics of the structure.

Moon says: Surely the default constructor function and "BOA constructors" must work compatibly. Unspecified initforms for optional and keyword arguments in a "BOA constructor" default from the slot initform rather than NIL, so "BOA constructors" face the same issue as default constructors.

VISIBLY-BOUND seems semantically wrong.

I would go with BOUND, assuming we can't just get rid of DEFSTRUCT entirely. I don't care about the supposed efficiency issue, which is easily gotten around or ignored.

-------

Edit History