[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
multiple values
Since I'm the one who added the procedures VALUES (a.k.a. RETURN) and
RECEIVE to T, it seems appropriate that I should say a word or two about
what the heck I had in mind when I did so.
In the T implementation, the low-level protocols for call to unknown
procedure and return to unknown continuation are almost identical. The
compiler does CPS conversion and doesn't see the difference much, and
the data representation for implicit continuations is the same as that
for procedures.
The implementation symmetry suggested that maybe some surface-language
symmetry was worth experimenting with. This was corroborated by the
frequent need for some way to return more than one result value to the
caller. I had been doing the latter with
(foo arg1 arg2 ... argn (lambda (val1 val2 ... valm) body)),
and this syntax seemed pretty unweildy (it doesn't indent nicely, for
example); using lists and destructuring wasn't much better. So in T2,
anticipating future compiler support in T3, a procedure VALUES was
defined to be more or less the same as LIST (actually closer to VECTOR
-- the details are irrelevant), and a macro RECEIVE was invented to get
result values:
(receive (val1 val2 ... valm)
(foo arg1 arg2 ... argn)
body)
== (apply (lambda (val1 val2 ... valn) body)
(foo arg1 arg2 ... argn))
The beauty of VALUES and RECEIVE is that they're totally noncommittal
about implementation. The above is a correct implementation of the
abstraction, but so is the T3 implementation, where
(define (values . vals)
(c-w-c-c (lambda (k) (apply k vals))))
and
(receive (val1 val2 ... valm)
(foo arg1 arg2 ... argn)
body)
== (receive-values (lambda () (foo arg1 arg2 ... argn))
(lambda (val1 val2 ... valm) body))
[maybe I have the argument order reversed, I alwasy forget]
and RECEIVE-VALUES is a new primitive procedure. (We'd never expect a
user to invoke RECEIVE-VALUES explicitly, but it has to exist so that
RECEIVE can be a macro rather than a special form. It would be
necessary in Scheme to exactly the same extent that MAKE-DELAY is
necessary, which is somewhat.)
Thus if T programs have to ported to some other Scheme dialect, or if T
decides at some future point that multiple-value-returns are a horrible
idea and should be retracted, all our code will still work because the
abstraction is noncommittal. The nice thing is simply the notation, not
the implementation. The fact that T3 implements it well encourages its
use (and makes GC's less frequent, which, unfortunately, is a concern),
but is incidental.
A small benefit of having this facility be primitive is that you have an
opportunity for some error checking not previously available, namely you
can get wrong-number-of-return-value errors. This is good for the same
reason that wrong-number-of-argument errors are good. But note that it
is a necessary condition in order for the facility to permit varying
implementations.
(I don't see strong reasons for adding destructuring to LET or for any
other linguistic support for the multiple return values besides VALUES
and RECEIVE.)
I'm still not sure how I feel about the feature. Certainly the
abstraction is a good idea. As for the semantical foundations, I feel
pretty strongly that call and return should be symmetrical. If you can
have many arguments, you should be able to return many values. But note
that (P -> Q) does not necessarily imply Q, it might imply not P. My
opinion now is that it's probably better to achieve symmetry by flushing
multiple-argument procedures rather than by introducing multiple-value
returns, but this is incompatible with the present shape of Scheme, so I
won't recommend it for this audience.
Gary gives good reasons for introducing immutable data structures.
Algol 68 and ML have the right idea; Lisp and Scheme got it wrong. It
would certainly be worthwhile to experiment with immutable pairs,
strings, and vectors. Adding immutable objects onto Scheme (especially
one type without the others) might be a mistake, and it could be the
case that adding immutable objects as an afterthought like this,
coexisting with mutable objects, would result in a very inelegant
language. (Maybe it could result in a very elegant language... anyone
for ((immutably cons) x y)? I don't know.) But I think this is a
question independent of the multiple-value question, and Gary's proposal
is both more complicated and has more far-reaching consequences than
RECEIVE and VALUES, which are trivial.
I like the idea of adding RECEIVE and VALUES to Scheme *without
specifying what happens when some VALUES go somewhere other than to a
RECEIVE*. The mechanism can be added trivially to any Scheme, and those
that want to optimize it (or which already do) may feel free.
Minimalists can implement the mechanism with lists or closures, Gary can
implement it with immutable vectors, Schemes which are embedded in
Common Lisp or cohabit with it may use CL's multiple values, and T can
do what it does. It is minimal and noncommittal, it's a pleasant
notation, it captures a common pattern of usage, so why not.
- Jonathan