The instance in my mind is this: what is better?
Example 1:
(define (foo x)
...
(values a b c))
(let-values (((a b c) (foo 42)))
...)
Example 2:
(define (foo x)
...
(list a b c))
(let ((f (foo 42)))
(let ((x (first f)) (y (second f)) (z (third f)))
...))
My rough guess is that first way is the best because in the second because whenever there is call of first
/second
/third
it has to iterate over the list. So my questione becomes: how does values
work? Is it only syntactic sugar for a list or it uses something else? (e.g. an array)
If that depends on the implementation I let you know I'm working with chicken scheme.
Multiple values (often abbreviated "MV") are a slightly controversial design choice of Scheme. They can be a bit unwieldy to use, which causes some people to call them "ugly". But they follow naturally out of the procedure-like interface that reified continuations offer. Let me unpack that a bit:
Whenever a procedure "returns", what really happens is that the value of the final expression in the procedure is delivered to its continuation. In this way, the expression that calls the procedure evaluates to a value.
For example: in (foo (+ 1 (* 2 6)))
, the continuation of (* 2 6)
will pass its value (that is the last expression inside the *
procedure, whatever it may be) as an argument to +
. Thus, in (foo (+ 1 <>))
, the <>
indicates the continuation of (* 2 6)
. Then, the continuation of (+ 1 (* 2 6))
in this example is to call foo
with 13
, the result of (+ 1 (* 2 6))
.
If you use call-with-current-continuation
to capture such a continuation, you get a procedure which you can call later. It's only natural that you should be able to call this procedure with multiple arguments; the only question is how an expression can actually result in multiple values.
More concretely,
(define (get-value) 1)
Is exactly the same as:
(define (get-value)
(call-with-current-continuation
(lambda (return-the-value)
(return-the-value 1))))
So, this makes sense too:
(define (get-3-values)
(call-with-current-continuation
(lambda (cont)
(cont 1 2 3))))
However, how should a call to get-3-values
work? Each expression always results in a single value (in other words, each regular procedure call's continuation accepts only a single value). That's why we need a special construct to create a continuation that actually accepts multiple values: let-values
from SRFI-71 (or receive
from SRFI-8).
If you want to use only standard constructs, you can use call-with-values
, but it's kind of awkward:
(call-with-values (get-3-values) (lambda (a b c) ...))
If you can call a procedure with multiple values (arguments), why shouldn't it be able to return multiple values? This makes sense especially considering the core of Scheme deals with continuations, which blur the line between "argument" and "return value".
Now, regarding when to use multiple values rather than an explicit list is a matter of taste. If the procedure conceptually has several return values it makes the most sense to return multiple values. For example, if you get the dimensions of a page in a PDF, a library could have a get-dimensions
procedure which returns two values: the width and the height. Returning them in a list would be somewhat strange. Of course, in this particular situation it might make more sense to change the design to have get-height
and get-width
, but if the operation is expensive to compute and yields both values, it makes more sense to have get-dimension
rather than the separate ones (because that would require either double the computation time or some complicated caching / memoization).
Specifically in CHICKEN there's one more design reason to use multiple value returns: it implicitly discards all values but the first if multiple values are passed to a continuation which accepts only one value (in standard Scheme "it is an error" if that happens, so implementations are free to do whatever they want)
For a practical example, the http-client egg (disclaimer: I'm its author) has procedures like with-input-from-request
which return several values, but the first value is usually the most useful / interesting: that's the result of reading from the URI. The other values that are returned are the URI of the request, which may differ if there were any redirects, and the response object, which is sometimes useful if you need to extract headers from the response. The upshot of all this is that you can do (with-input-from-request "https://stackoverflow.com" #f read-string)
to receive the string containing Stack Overflow's homepage, without even bothering to deal with constructing request objects or picking apart response objects, but you have the option to do so if you need to do more advanced things.
Note that multiple values might be implemented inefficiently in some Schemes, so for practical reasons it might be more efficient to return an object like a list, vector or record (in CHICKEN, MV are fast so that's not an issue).