[plt-scheme] Re: to define, or to let

From: Joe Marshall (jrm at ccs.neu.edu)
Date: Tue Apr 27 15:12:31 EDT 2004

Paul Schlie <schlie at comcast.net> writes:

>   For list-related administrative tasks:
>   http://list.cs.brown.edu/mailman/listinfo/plt-scheme
>
>> Joe Marshall wrote in response to Bill Richter:
>> ....
>
> Rather than hashing through each point, please consider the following
> observations to your various points:
>
> - evaluation order refers to "evaluation", if the author of a macro chooses
> to effectively reorder arguments prior to their evaluation, that¹s their
> prerogative; however regardless of their choice, the resulting program
> should be semantically (behaviorally) unambiguous, which can only be
> achieved by specifying evaluation order.

There are other ways of making it behaviorally unambiguous.

My point in regards to macros is this:

  Although macros are simply a code->code transformation, many macros
  are used to simulate normal-order exceptions to the usual
  applicative order evaluation.  It is not infrequent that you see a
  macro written with some variant of this:

  (define-syntax some-macro
    (syntax-rules ()
      ((some-macro subform ...)
       (some-function (lambda () subform) ...))))

  The resulting use of the macro *exactly* resembles a function call.
  It *is* a function call, but with normal-order evaluation.

  The user of a such a macro needs to understand the *effect* of the
  macro, but he should not be required to understand the *details* of
  the expansion.  Any expansion with equivalent effect ought to be
  valid.

  Now comes our macro writer Louis Reasoner who wants to add C-style
  iteration to Scheme.  He writes this macro: 

  (define-syntax for
    (syntax-rules ()
      ((for (var start-form limit-form) forms ...)
       (let ((limit limit-form))
         (let loop ((var start-form))
           (if (< var limit)
               (begin
                   forms ...
                   (loop (+ i 1)))))))))

   (Note that this macro appears in   collect\net\uri-codec-unit.ss,
   so this is a real-life example.)

   Louis documents this macro to iterate from START (incusive) to END
   (exclusive) evaluating FORMS in an environment where VAR is bound
   to the current iteration value.

   Cy D. Fect loves this new macro because he thinks iteration is
   superior to recursion.  He wants to print out and clear a buffer.

   (for (index (get-lower-bound) (begin0 (get-upper-bound)
                                   (clear-bounds)))
     (display (fetch-at-index index)))

   This appears to work, but for some reason *always* starts at offset
   zero.

   The reason, of course, is that the LIMIT form is evaluated *before*
   the START form.  In the form:

   (for (<var> <start> <limit>) <body> ...)

   It is obvious that the following is true:

     - FOR is a syntactic marker (keyword) and isn't evaluated.

     - VAR is a variable, it is bound by the form like in a LET, and
       it is not evaluated.

     - START and LIMIT are `standard' forms and *are* evaluated as one
       might expect.

     - BODY is made of `standard' forms, also evaluated as one might
       expect.

   How do we explain the problem to Cy and/or Louis?

   1.  Explain that although evaluation order is *mandated* left to
       right, FOR is a macro and therefore is exempt from the mandate.

       So now Cy has two models of evaluation:  left to right for most
       things, but *unspecified* (unless otherwise noted) for macros.

       Cy will still be surprised by things like this:

          (define-syntax array2d-ref
            (syntax-rules ()
              ((_ a i1 i2)
               (let ((bound (vector-ref a 0)))
                 (if (or (< i2 0) (>= i2 bound))
                     (error 'array2d-ref 
                            "index ~a out of range [0, ~a] for array2d"
                            i2
                            (sub1 bound))
                     (vector-ref a (+ 1 i2 (* bound i1))))))))

        (taken from collects\parser-tools\private-yacc\array2d.ss )

       It *looks* like a function, but it isn't.


   2.  Explain to Louis that because evaluation order is *mandated*
       left to right, the burden is upon him to ensure that it is
       maintained wherever it *syntactically seems* that evaluation is
       occurring. 

       Now Cy can still use his `left to right evaluation' model, but
       when Louis goes to implement his list-comprehension package
       (see srfi-42), he has to fix things such that this:

       (fold3-ec (error "wrong") (:range i 10) i (make-mapper) (make-combiner))

       doesn't do something surprising like call `make-mapper' once,
       but make-combiner multiple times.  (which is what srfi-42
       currently does)

    Neither one of these is very satisfying.  The R5RS option is this:

    3.  Explain to Cy that order of evaluation is *not* specified and
        that it is therefore an error to rely on sequencing anywhere
        but within a BEGIN expression and especially not to place side
        effecting forms in arguments to functions or macros.

        In other words, tell Cy that he *cannot* expect to write code
        like this:

       (...   (get-lower-bound) (begin0 (get-upper-bound)
                                     (clear-bounds))
          ...)

        and have it work.

> - if two programs (utilizing any combination of expressions and/or macros),
> are equivalent under R5RS, and not if argument order is specified; then
> those same two programs are behaviorally ambiguous under R5RS, therefore
> meaningless to no one's benefit; as opposed to having a well defined
> meaning/behavior otherwise if evaluation order were specified.
>
> Rather than continuing to argue about the hypothetical merits of unspecified
> evaluation order, I'll abstain, and wait for someone to produce a code
> fragment which benefits from it's resulting ambiguity.
>
> (which I am confident no one will be able to produce)

There are many things that are meaningless under R5RS:  (car 42),
(call-with-current-continuation), etc.

Currently, there is no valid code that depends on the behavior or
return value of (car 42), so it is meaningless to no one's benefit.
Would it not be better to have all functions defined as the identity
function when applied to an argument not within their domain?  I am
confident that no one will be able to produce a code fragment that
*benefits* from the current ambiguity of (car 42).

I see no particular `benefit' to not specifying the order of
evaluation, but then I see no `benefit' to specifying it, either (and
I am confident that no one will produce such).  I do, however, see
drawbacks to specifying it.  In particular, it does not make it easier
to reason about side effects because it only enforces left to right
order in certain cases.  It does not remove side-effect dependencies
between argument evaluation.  It doesn't solve the problem that it is
purported to solve.  Furthermore, it *encourages* sloppy coding
because side-effect dependencies *can* be intentionally depended upon
and *cannot* be easily discovered if accidentally depended upon.  It
complicates the evaluation model because the order cannot be enforced
everywhere.  Finally, it prohibits simple code rewrites that are
useful to understanding.

I don't think there are merits to unspecified order.  I think that
that specified order has all the drawbacks that unspecified order has,
and some additional ones.




Posted on the users mailing list.