[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Multiple Values: An Opinion
Multiple Values: An Opinion
----------------------------
Values Objects
--------------
My view on multiple values is based on the principle that everything in
the language should be first class. For multiple values I postulate an
*IMMUTABLE* values object type. Values objects are created by the
values function. So,
(values <v0> ... <vN>)
creates a values object with values <v0> ... <vN>. Since values
objects are first class objects it would makes sense to bind a values
object to a (single) variable or cons up multiple value objects in a
list.
Components of a values object can be explicitly extracted with the
values-ref function. Like vectors, values object indexing is 0-based:
So,
(values-ref (values <v0> ... <vI> ... <vN>) I) ==> <vI>
Note that the function single can be simply defined as:
(lambda (vals) (values-ref vals 0))
The number of values in a values object can be computed with the
function values-length. Eg.
(values-length (values <v0> ... <vN>)) ==> N+1
Lastly, values objects can be identified by the predicate values?.
Values Objects and Identity
---------------------------
Since values objects are immutable they are intentionally
(operationally) identical (eqv?) if they have the same number of values
and if all the values are intentionally identical. Thus,
(eqv? (values <x1> <x2>) (values <y1> <y2>)) ==> #T
iff (eqv? <x1> <y1>) ==> #T and
(eqv? <x2> <y2>) ==> #T
Because of their immutability, The extensional identity (eq?-ness) of
multiple values is left unspecified. The contention here is that asking
the eq?-ness of values objects is uninteresting (maybe even
meaningless). It would be analogous to asking whether two bignums are
eq?.
Returning Values Objects
------------------------
A Values object is returned (directly or indirectly through a
continuation) just like any other type of object is returned.
Applying Functions to Values Objects
------------------------------------
Multiple-value-call takes a function and an expression which evaluates
to a values object and applies the function to the values in the values
object.
(multiple-value-call (lambda (x y) (+ x y)) (values 1 2)) => 3
Also, LAMBDA is augmented so that a lambda expression with a single
(unparenthesized) formal parameter (eg. (lambda x x)) is applied via
multiple-value-call, the formal parameter is bound to a values object of
all the actual parameters (Note BVL). Thus,
(multiple-value-call (lambda x x) (values 1 2)) <=> (values 1 2)
(Note: In (apply (lambda x x) ...), x would get bound to a list,
while in (multiple-value-call (lambda x x) ...), x would get bound to a
values object. I'm not enthused about the overloading of the single
unparenthesized bvl variable, but could not think of better syntax.)
Summary
-------
First class multiple values, values objects, are desirable for several
reasons:
1) It simplifies specifying which forms pass multiple values
through. Simply put, multiple values are passed through any form just
like any other values.
2) It simplifies from where multiple values can be returned. They can
be returned from anywhere. In contrast, in Common Lisp multiple values
can only be returned from the last form in an OR expression. In this
scheme multiple values can be returned from any form in an OR
expression.
3) In contrast to the original, thunkified proposal, the forms that
expect multiple values (multiple-value-call, single, etc.) could be
redefined such that the cumbersome multiple value thunks are replaced
with a simpler multiple value expression.
Eg. (single (lambda () (mvs ...))) => (single (mvs ...))
4) It simplifies the interaction with call/cc. To return multiple
values from a continuation just incant (cont (values <v0> ... <vN>)).
5) It allows programmers to manipulate multiple values without having
to inefficiently and unaesthetically coerce them to and from list structures.
Consider:
Expression oriented as opposed to thunk oriented.
(set! l (set! l
(cons (mvs ...) ...)) (cons
(multiple-value-call
list
(lambda () (mvs ...))
...)))
(multiple-value-call f (car l)) (apply f (car l))
Efficiency considerations
-------------------------
The immutability of values objects allows the compiler in cohoots with
the runtime system much latitude in representing multiple values. A
compiler/runtime strategy could be to return multiple values spread out
on the stack or in registers. When multiple values are returned to a
context which expects them the corresponding values object would never
have to be reified. For example, in
(define mvs (lambda (a b c) (values a b c)))
(multiple-value-let
(((x y z) (mvs 1 2 3)))
...)
a values object would not be allocated. Value objects would only have
to be allocated in the heap when multiple values are returned to a
context which is not expecting multiple values. For example,
(cons (values <v0> ... <vN> ) ...)
would require a values object to be allocated. However, a smart
compiler could avoid allocating a values object in many situations where
multiple values are indirectly expected. For instance, if the
equivalent of Common Lisp's (multiple-value-prog1 <exp1>... <expN>) was
implemented as:
(let ((x <exp1>))
<exp2>
...
<expN>
(multiple-value-call values x))
a smart compiler could avoid the allocation of the multiple values bound
to x by:
1) Leaving the multiple values returned by <exp1> spread out on the
stack.
2) Evaluating <exp2> through <expN>.
3) returning the spread out values of <exp1>.
Similar optimizations could be applied to multiple values returned by
know continuations, like cont, in:
(call/cc
(lambda (cont)
...
(cont (values <v0> ... <vN>))
...))
where the presence of the return in argument position would normally
require a values object to be allocated.
In even more esoteric situations, the compiler could represent (a
single group of) multiple values in a values object as well as spread out
on the stack. For example, in
(let [[[x (mvs ...)]]]
; spread x out on the stack for
; fast access
(multiple-value-call frob x) ; use the spread out values
(foo x) ; use the values object bound to x
(multiple-value-call frob x) ; use the spread out values, since
; we don't have to worry about
; foo side-effecting the values
; in x.
...)
My Answers to the Survey
------------------------
1) (a & b) (multiple-value-call <f> <mv-expression>)
(c) (values <v0> ... <vN)
2) Make multiple-value-call and values essential.
3) Include single as (single <mv-expression>) and make it non-essential.
4) Don't allow multiple multiple-value expressions in
multiple-value-call.
5) call/cc is not a problem with first class values objects.
6) Multiple values (i.e. values objects) are passed through forms just
like any other value is.
7) Just return a (first class) values object.
8, 9 & 10) Include:
(multiple-value-let (((ids*) form)*) <body>),
(multiple-value-let* (((ids*) form)*) <body>),
(multiple-value-letrec (((ids*) form)*) <body>) (maybe),
(multiple-value-set! (ids*) <form>), and
(multiple-value-define (ids*) <form>)
(maybe with mv-* nicknames) as non-essential. In any of the constructs
signal an error if too many or too few values are returned.
11) a) Include (values->list <mv-expression>) as non-essential.
b) Include (list->values <list>) as non-essential.
c) Include (values->vector <mv-expression>) as non-essential.
d) Include (vector->values <vector>) as non-essential.
e) other
include (values-ref <values-object> <index>) as essential
(values? <object>) as essential