[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: n-ary functions rfc
Date: Fri, 12 Jun 1992 13:34:59 -0700
From: david carlton <carlton@husc.harvard.edu>
I forwarded a copy of the n-ary functions proposal to a friend of
mine, and his response was (essentially) "why not just use length and
list-ref"? I tend to agree with him - rather than introduce new
syntax to support another method of handling optional arguments, why
not use the existing syntax and procedures to support that method? I
don't think that it would be any more tedious to use length and
list-ref than to use the proposed method, and I would think that
compilers would be able to optimize either way equally well - the only
thing that I can see preventing them from optimizing the uses of
length and list-ref is the lack of some way to guarantee that they
haven't been redefined by the user, but in practice the Scheme
compilers that I'm familiar with will inline standard procedures and
at any rate once a module system is added (if one ever is...), that
problem would go away.
Before I respond to this suggestion, let me point out that there are several
reasons why it is better to represent n-ary function arguments with a function
rather than a list, even aside from the potential for optimization:
* Procedures are a more fundamental data type in Scheme than lists are;
we have special expressions for creating and using them. Aside from
history, the choice of lists for the representation of extra
arguments seems not just arbitrary, but foolish: vectors are clearly
more efficient to allocate, initialize, and access, regardless of the
order of the accesses.
* Procedures are immutable, making them easier to reason about than
lists; even if the procedure encapsulating the extra arguments
`escapes' to an unknown context, the reader of the program can rest
assured that no change in its `contents' will take place.
* Procedures are a more flexible representation of a sequence of
values; they can, for example, be composed with one another to
compute a new, modified version of a given sequence in constant time,
regardless of the number of elements. Also, the computation of the
elements can be done lazily, regardless of the order in which they're
requested. Having the extra arguments represented as a procedure
makes it very convenient to use the expressive power of the FAPPLY
function included in my proposal.
Now, as for optimizing the use of a list holding the extra arguments, I have a
couple of points to make.
First (and I realize that this isn't what you're suggesting, David), many brash
Lisp compiler hackers have for years claimed that it should be
`straightforward' to optimize the use of CAR, CDR, and PAIR?/NULL? in dealing
with rest lists. `All' you have to do is a little data flow analysis to
maintain extra integer index variables along with the various sublist values
floating around. Then a use of CAR can, in theory, simply be transformed into
an operation using the corresponding index to access the value on the stack.
As far as I know, however, not a single one of these brash compiler hackers
(myself included, since I also used to make this claim) has ever succeeded in
implementing such an optimization.
Even if they had, though, my second point would stand. (I do understand that
performing the optimization for the LENGTH/LIST-REF case is much easier.) That
is, it isn't portable for me to write code that counts on such an optimization
being made, since surely not all compilers will do so. But how, I hear you
ask, can my code `count' on an optimization?
Well, clearly the presence or absence of an optimization can't change the final
outcome of my program, but it certainly *can* have so great an effect on the
performance of my program as to render it useless in non-optimizing
implementations.
For example, consider this implementation of n-ary addition:
(define (+ . args)
(let ((len (length args)))
(case len
((0) 0)
((1) (list-ref args 0))
(else
(do ((i 1 (bin+ i 1))
(sum (list-ref args 0)
(bin+ sum (list-ref args i))))
((= i len)
sum))))))
Now, in an implementation that supported the optimization you're suggesting,
David, this code would probably run quite nicely and might even be worth
considering for the `official' implementation of + in that system.
If this code is ported to a different implementation, though, in which the
optimization is *not* provided, then the function performs abominably: it
conses wildly (and uselessly) and, much worse, it is no longer even *linear* in
the number of arguments (because list-ref is not a constant-time operation)!
This unavoidable kind of performance non-portability (changing not just the
constant factors in my program's running time, but the asymptotic bound) is, I
believe, unacceptable.
The major advantage of the procedural representation here is that the required
optimization for such simple cases is so incredibly easy to provide that one
has a significant chance of many implementations doing so (and even if the
optimization is *not* provided, no change in the asymptotic bound can accrue).
I don't believe that any reasonable argument can be made that many
implementations will so optimize either the CAR/CDR/NULL? or LENGTH/LIST-REF
idioms; Scheme and Lisp implementations have existed for a long time now and
nobody's seen fit to do it yet.
In summary, then, procedures are better than lists as a representation of extra
arguments because they're
(1) simpler in the sense of conceptual parsimony,
(2) easier for both humans and compilers to reason about,
(3) more flexible (especially with the addition of FAPPLY), and
(4) much easier to optimize.
As far as I can see, the only thing lists have going for them is history.
Pavel