CLHS: Issue DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES Writeup

#

## Issue DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES Writeup

    Forum:		Cleanup
    Issue:		DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES
    References:	DEFSTRUCT; CLtL p. 309
                    Issue DEFSTRUCT-CONSTRUCTOR-KEY-MIXTURE (passed)
    Category:	CLARIFICATION, CHANGE
    Edit History:   V1, 11 Oct 1989, Sandra Loosemore
    		V2, 02 Nov 1989, Sandra Loosemore (update discussion)

    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 (DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES: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 (DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES: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 (DEFSTRUCT-CONSTRUCTOR-SLOT-VARIABLES: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.

    -------

Copyright 1996-2005, LispWorks Ltd. All rights reserved.