[R6RS] summary of decisions regarding multiple values
dyb at cs.indiana.edu
dyb at cs.indiana.edu
Fri Jun 23 12:15:58 EDT 2006
Thanks for making a concrete proposal. I still prefer that an exception
be raised when fewer or more than one value is returned to a continuation
not created by begin or call-with values, but at least we now have
something to compare against.
In my experience, returning an unexpected number of values is a common
programming error. I make both kinds of errors myself, especially when
modifying existing code. Your proposal would make such errors more
difficult to debug. Back when this issue first arose, my Common Lisp
experience was fresh in my mind. I had all kinds of problems with masked
multiple-value errors in working with someone else's Common Lisp code, and
that's why I became a strong advocate of the error checks.
Furthermore, it's inconsistent to require that an expected number of
values be passed to a procedure but not require the procedure to return an
expected number of values. To quote from your earlier note, "the editors
will have an obligation to explain why they want to require sloppiness in
some contexts but forbid that same sloppiness in others."
> > Good question. Here's my answer. If we're going to require that a begin
> > continuation ignore a single, arbitrary value, it makes sense that it
> > ignore all of the values. In other words, using begin is essentially a
> > declaration that the values don't matter and should be ignored. For a
> > continuation that doesn't ignore at least one of its values, it makes
> > sense to require that it be passed the right number of values.
> Those requirements make sense in the sense that they are
> not complete nonsense, but not being complete nonsense is
> not the only property people expect of a design. We need
> a genuine rationale.
I believe it is a genuine rationale; whether it convinces you or not is
another matter. Maybe I need to spell it out a bit more. It doesn't make
sense for begin to ignore just one, arbitrary return value. The other
solutions I mentioned would break many R5RS programs. Having begin ignore
all of its return values is the only reasonable solution. (We seem to
agree on this point.) Similarly, when a continuation specifies the number
of values it uses, it often doesn't make sense for the implementation to
patch up the situation by manufacturing a value (in the zero values case)
or discarding additional values (in the two+ values case). Based on my
experience, doing so just masks likely program errors, and requiring the
implementation to raise an exception is the only reasonable way to address
On the other hand, it's easy for programmers to say explicitly when they
don't want the additional return values, using the following wrapper from
Ashley and Dybvig's 1994 LFP paper:
(lambda () expr)
(lambda (x . y) x)))))
One could even use a modified version that returns the "unspecified" value
in the zero values case:
(lambda () expr)
((x . y) x()))))))
> > One reason is to detect a common programming error. Another is to help
> > ensure that programs intended to be portable don't count on
> > implementation-specific behavior, like ignoring all but the first value
> > passed to a single-value continuation or manufacturing some arbitrary
> > single value when no values are passed to a single-value continuation.
> Those are two rationales. The first is genuine, and I
> will look at it more carefully in a moment. The second
> is a genuine rationale for having some well-specified
> semantics, but it is not a genuine rationale for any
> particular semantics.
> In particular, my first proposal resolves the portability
> issue at least as well as the semantics you described.
That much is true.
> In a practical sense, I believe my first proposal would make
> programs more portable than with the other semantics,
> because four or five of the world's six and a half major
> Scheme compilers are unlikely to implement the semantics
> you described, even if that makes them non-compliant.
Some implementations, perhaps some of the ones you refer to, won't
implement proper tail recursion, full continuations, or other R6RS
features their implementors don't care for or can't figure out how to make
efficient enough for their tastes in the amount of time they're willing to
spend on the them. Do we really want to base our language design on that
kind of implementation?
Some of those implementors probably won't even bother to implement a safe
mode at all, so a requirement to raise an exception won't affect them.
> Any such prohibition would introduce an irregularity
> into the language and violate the principle "that a
> very small number of rules for forming expressions,
> with no restrictions on how they are composed" is
> what Scheme is about.
This discussion has nothing to do with expressions and the ways in which
they can be composed. (car (values 3 4)) remains syntactically
well-formed, just as is (cons 3). The discussion boils down to whether
the former raises a run-time exception. We both agree that the latter
> When I compare the semantics described at the top of
> this essay to the semantics that Kent described, the
> semantics I favor wins out with respect to simplicity
> (of semantics, use, and implementation), consistency,
> tradition, reusability, interoperability, and efficiency.
This is an impressive litany, but a false one. It is not simpler to use if
it masks programming errors that cause programmers to spend extra time
debugging. It's not simpler to use if a programmer looking at someone
else's code doesn't know if a value is being ignored intentionally or by
accident. It's not consistent to check the number of procedure arguments
but not the number of return values. You have me on tradition if we
go back far enough, but some of the most popular Scheme systems have
checked the number of return values for many years, so more recent tradition
isn't so clear. In any case, if we were bound by tradition, there are a
number of changes we would never have made, like using ? instead of p
to indicate predicates, ! to indicate mutation, #f instead of NIL to
indicate false. (There are many other examples, too numerous to mention.)
We could similarly enhance reusability and interoperability by making
every procedure accept one list argument and return a list of values, but
the increase in reusability and interoperability would be illusury, as
interface errors would be similarly masked and/or simply result in some
other error somewhere else. You may have me on efficiency for certain
implementation techniques (in safe mode), but that's true of all of our
error-checking requirements, including the requirement to check the number
of arguments to a procedure. Why is this case special?
> My second proposal above is one immediate benefit:
> procedures that return no useful value could return
> zero values without breaking any R5RS-portable code.
That is nice.
> Another benefit is that transformations from source
> code into CPS and A-normal form, and optimizations
> of those forms that appear throughout the research
> literature, will continue to apply to Scheme.
Not exactly. The traditional CPS form of (lambda () (f (g))) is
(lambda (k) (g (lambda (v) (f v k)))). With your proposal, g can pass any
number of arguments to its continuation in the original but must pass
exactly one value to its continuation in the CPS version. You could gum
up the CPS to read (lambda (k) (g (lambda (v . r) (f v k)))), but
handling the rest arguments well or at all may require modifications to
the optimizations that appear in the literature.
> the semantics Kent described, almost all of those
> transformations would break, as would compilers that
> use them. Scheme would lose much of its traditional
> usefulness for research into program transformations.
Any compiler that handles call-with-values and values will have to
generalize their CPS and A-normal forms and optimizations on those forms
anyway, so handling the (other) single-value return cases properly should
present no additional problems.
In either case, research can continue to proceed, as it usually does,
under the assumption of a fixed number---often one---of arguments and
> With the semantics proposed at the top of this rant,
> the div procedures could return both the dividend and
> remainder as values, and the mod procedures could
> return both the remainder and the dividend. All of
> the div+mod procedures would become equivalent to the
> div procedures, and would go away. Similarly the
> fixnum+, fixnum*, and fixnum- procedures could return
> both the low-order bits and the carry, making the
> fixnum+/carry, fixnum*/carry, and fixnum-/carry
> procedures redundant.
I agree it would be nice to get rid of this dozen or so primitives, but
not at the expense of what I see as useful error checking.
An alternative is to introduce value selectors, say first, second, and
nth, and flush both the div+mod and mod procedures. Then one would write
(first (div x y)) or (second (div x y)) for the dividend or modulus. The
code would be more verbose in most places where these procedures are used,
but there would be even fewer of them. We could shorten this to
(first div x y) and (second div x y) to eliminate some of the additional
> This would be just as efficient as having all of the
> SRFI 77 procedures, because it is easy for a compiler
> to recognize a known call to fixnum+ (for example)
> whose context obviously ignores the carry, and turn
> it into machine code that doesn't generate the carry.
Some of the same implementors who refuse to implement return-value
checking (and full continuations and tail recursion) might also refuse to
implement these because it's too inefficient to compute and return both
values and too much trouble to detect when only one value is needed.
More information about the R6RS