4

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.

Andrea Ciceri
  • 436
  • 6
  • 16

3 Answers3

8

Some background on where the idea of multi-values comes from

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) ...))

tl;dr

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".

Design choices: when to use MV and when not?

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).

sjamaan
  • 2,282
  • 10
  • 19
2

From a semantic point of view there is not one better than the other. You can make use of lists, vectors, and records and they will give the same features. IMO a hash would be slightly favourable since then you have accessors and thus an interface can be increased later. JavaScript can only return one value so it uses objects and with the new restructuring operators it has become quite usable.

Scheme didn't invent much except the lexical closures so much is derived from earlier lisp versions. Just like the loop macro being able to return multiple values means you don't need to allocate short lived objects on the heap that need GC-ing later. At the time it came to be machines had very little memory and GC-ing were sometimes done with disk. On the machine level a return usually was something left on the stack or in a register. Multiple values would then be two values on the stack or use of two registers. No heap allocation for the encapsulation meant no allocation of short lived memory.

In a sibling language, Common Lisp, the multiple values are more usable since every function can be used in a one value place and then use only the first value returned. You see the lack of this by the standard library in Scheme having several functions doing the same. quotient/remainder that returns two values, and quotient and remainder that returns each of the two values.

Sylwester
  • 47,942
  • 4
  • 47
  • 79
0

Multiple return values are actually distinct from lists, which is a little weird. values, let-values, define-values, etc. are for dealing with multiple return values.

If you use pattern matching macros, e.g. from the match egg, then you would be able to do exactly the same thing when returning lists.