[ERR5RS] Rationale and ease of use

Lynn Winebarger owinebar at gmail.com
Tue Sep 11 01:27:58 PDT 2007


On 9/8/07, AndrevanTonder <andre at het.brown.edu> wrote:

>  On Sat, 8 Sep 2007, Lynn Winebarger wrote:
>
> > suppose the intent was to remove some layers of nesting, but the effect in
> > R6RS has been taken to the extreme - top-level defines have been redefined
> > to be consistent with internal defines!
>
> No.  Actually R6RS internal definitions are now consistent with *R5RS* toplevel
> definitions (except for forbidding internal redefintions, which R5RS also did).
> So R6RS is more consistent with R5RS than R5RS was with itself ;-)

     I don't think so, except that R6RS mandates left-to-right
evaluation of letrec bindings, which is just another gratuitous
change.
     I refer to
http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-8.html#%_sec_5.1
:
      [from 5.1]


      Definitions and syntax definitions occurring at the top level of
a program
       can be interpreted declaratively. They cause bindings to be created in
       the top level environment or modify the value of existing
top-level bindings.
       Expressions occurring at the top level of a program are interpreted
       imperatively; they are executed in order when the program is invoked or
       loaded, and typically perform some kind of initialization.

     R5.97RS has the following mandate for top-level defines (from
http://www.r6rs.org/document/html-5.97/r6rs-Z-H-13.html#node_chap_10
) :

     Because definitions and expressions can be interleaved in a
     <top-level body> (see chapter 8), the expander's processing of a
     <top-level body> is somewhat more complicated. It behaves as
     described above for a <body> or <library body> with the following
     exceptions. When the expander finds a nondefinition, it defers its
     expansion and continues scanning for definitions. Once it reaches
     the end of the set of forms, it processes the deferred right-hand-side
     and body expressions, then generates the equivalent of a letrec*
     form from the defined variables, expanded right-hand-side expressions,
     and expanded body expressions. For each body expression
     <expression> that appears before a variable definition in the body, a
     dummy binding is created at the corresponding place within the set
     of letrec* bindings, with a fresh temporary variable on the left-hand
     side and the equivalent of (begin <expression> <unspecified>),
     where <unspecified> is a side-effect-free expression returning an
     unspecified value, on the right-hand side, so that left-to-right
evaluation
     order is preserved. The begin wrapper allows <expression> to evaluate
     to an arbitrary number of values.

    This is considerably different in intent from the above.
According to Will Clinger,
 (http://lists.r6rs.org/pipermail/r6rs-discuss/2007-March/001831.html
), the high-level (syntax-rules, to be specific) macros specified in
R5RS are indifferent to when the expansion is actually performed.  It
should be clear the above specification of R6RS is all about making
syntax-case macros work consistently with separated phases rather than
consistently with prior notions of Scheme.   Despite allowing defines
to be interspersed at the top-level, those defines might as well be
internal defines (modified for letrec* order of evaluation)..

> > (define bar 1)
> > (define foo
> >   (lamdba (x)
> >      (if x
> >          (define bar 100)
> >          (set! bar (+ bar 1)))
> >      #t)))
> >
> > There is one sensible, consistent way to interpret this code, which is that
> > the (define bar 100) establishes a global binding of bar, not a local one.
>
> No.  Even if you could have a definition as a branch of IF (which you cannot),
> the LAMBDA introduces a local scope, so assuming that DEFINE is a binding
> form, the inner BAR would not be visible to the toplevel.  The concept of local
> scope has been part of Scheme forever and is in fact part of the original
> raison d'etre of Scheme.

      This was posed as a thought experiment in language design.  For
the purposes of the experiment, I took the sentence "Scheme
demonstrates that a very small number of rules for forming
expressions, with no restrictions on how they are composed, suffice to
form a practical and efficient programming language that is flexible
enough to support most of the major programming paradigms in use
today.", that has appeared in all RnRS for n>=2, as being inviolate.
        With that rule in place, the restrictions on internal defines
would have to be discarded as they directly conflict with it.
Therefore, in our gedanken Scheme, having define in a branch of if
must be perfectly sensible even in local scope.
       While you are correct (of course) about lambda introducing
local scope, that local scope does not contain a binding of BAR.
   > (define bar[*] 1)
 > (define foo
 >   (lamdba (x)
 >      (if x
 >[D]          (define bar[^] 100)
 >[M]          (set! bar (+ bar 1)))
 >      #t)))
     I have annotated the program to identify the spots of interest.
You left out the invocations of foo that illustrate why my suggested
interpetation is necessary.
 [1] (foo #f)
 [2] (display bar) => 2
  [3] (foo #t)
  [4] (display bar) => 100
  [5] (foo #f)
  [6] (display bar) => 101

         In [1], the second branch is taken, and the [M] statement is
executed.  Since the [D] statement has never executed, bar naturally
scopes to the outer definition.  Line [2] follows.
         In [3], the first branch is taken, and the define form is
executed.  Does this define extend the local scope with the variable
bar, or does it assign a new value to bar[*]?  We will leave this for
the moment.
         In [5], the second branch is taken.  Consistency requires
that (a) bar refers to the same binding as in [1], and (b) that it
refers to the same binding as bar[^] from [3].
         If [3] causes a new binding to be added to the local scope,
requirement (b) is in trouble if (a) is followed.

    The most obvious counter to this line of argument is that the
defines are meta-operations (extending environments at compile time),
so bar would have a local binding in all instances.  On the other
hand, if the "(if x" became "(if #f", we could not immediately discard
the branch for truth without checking for definitions.  This seems
unpleasant to me, but it might be palatable for others.[*]
     While I have been writing this out, the following post on
comp.lang.scheme occurred:
http://groups.google.com/group/comp.lang.scheme/browse_thread/thread/cf6204666533c68c/#
, showing that this notion of define has some pragmatic appeal.

[*]  R6RS question: What is the result of
(define foo #f)
(define bar
    (lambda ()
         (if #f
            (let-syntax ((fuzz
                                 (begin
                                     (set! foo 1)
                                     (syntax-rules x () ((_  y ...) 1)))))
                 <whatever>)
            (let-syntax ((fuzz
                                (lambda (x)
                                   (syntax-case x ()
                                       ((keyword y ...) (datum->syntax
#'keyword foo))))))
                      fuzz))))

(bar) => ???

I don't know the answer, but let's assume you take whatever steps are
necessary in your implementation of choice so that foo is defined in
the same phase as the expansion of the definition of bar.

> > the top-level definitions (in the presence of syntax-case, anyway) are
> > effectively evaluated in phases above the expansion of all subsequent
> > expressions, with bindings shared in downward phases.
>
> This is only true in a REPL setting.  To extend this to a compiler-only
> Scheme implementation is possible, but would require the compiler to evaluate
> each definition before continuing to compile subsequent expressions.
> Imagine the following program:
>
>    (define pi-to-four-billion-digits
>      (calculation-taking-40-days))
>
>    (define-syntax format-result
>      (lambda (e) (syntax-case e -----)))
>
>    (format-result pi-to-four-billion-digits)
>
> Adopting your suggestion, /compiling/ this program will take 40 days.  And then
> you have not even run it.
>

      It would only take 40 days if pi-to-four-billion-digits was
required at expand-time in one of the subsequent expressions.  In
which case it should take 40 days, or compilation should be deferred
to run-time.  I trust the author understands the ramifications of
his/her code, and intended it to be so.

>   > It is a shame
> > R6RS will not explicitly state what programs will have a definite meaning,
> > and explicitly leave the rest undefined.
>
> Exactly!
>
> > (define bar 10)
> > (define-syntax foo
> >  (lambda (x)
> >     (define bar 'a)
> >     (syntax-case x ()
> >       ((_) #'bar)
> >       ((_ y) #'(set! bar y)))))))
>
> Again, the lambda introduces a local scope, so the inner definition of BAR
>  cannot modify the outer BAR (since forever in Scheme).

    First: Your response assumes "define" should have something to do
with the scopes introduced by lambdas.  "define" as it exists at
top-level is anathema to lambda-introduced scopes.  Just for
specificity, "forever"=R2RS.
     Second: Your response appears to miss the point of the example.
I am probably to blame for that because I used internal-define instead
of let-binding the inner BAR.  The point is that the macro should
expand to code referencing and modifying the expand-time binding of
BAR, not the global binding at all.  I claim that is the lexically
apparent binding for the person reading the code.  Indeed, if you
review the original definition of LISP, the reason for dynamic scoping
is the mechanism for abstraction: the lambda expression is simply
quoted and then evaluated in the extant environment when invoked.  The
separate binding model is a much more complex implementation of the
same type of phenomenon.

Lynn
PS.  I am posting this to the wiki.  It is my hope to illustrate
divergent opinions to make sure ERR5RS leaves undefined things that
should remain undefined.



More information about the Err5rs mailing list