The Explorer

The Adventures of a Pythonista in Schemeland/16

by Michele Simionato

February 17, 2009



There is a feature of Scheme that I never liked, i.e. the fact that functions (more in general continuations) can return multiple values. Multiple values are a relatively late addition to Scheme - they entered in the standard with the R5RS report - and there has always been some opposition against them (see for instance this old post by Jeffrey Susskind). I personally see multiple values as a wart of Scheme, a useless complication motivated by premature optimization concerns.

It is possible to define functions returning multiple values as follows:

> (define (return-three-values) (values 1 2 3)) > (return-three-values) 1 2 3

In order to receive the values a special syntax is needed, and you cannot do things like the following:

> (apply + (return-three-values)) Unhandled exception Condition components: 1. &assertion 2. &who: apply 3. &message: "incorrect number of values returned to single value context" 4. &irritants: ((1 2 3))

Instead, you are forced to use let-values or other constructs which are able to accept values:

> (let-values (((x y z) (return-three-values))) (+ x y z)) 6

In this series I will never use functions returning multiple values, except the ones in the Scheme standard library (this is why I am forced to talk about let-values ). Instead of using multiple values, I will return a list of values and I will destructure it with let+ . For instance, I will write

> (let+ ((a b) (list 1 2)) (cons a b)) (1 . 2)

instead of

> (let-values (((a b) (values 1 2))) (cons a b)) (1 . 2)

let+ is more elegant and more general than let-values : everything let-values can do, let+ can do too. let+ can even faster - in some implementations and in some cases. Here is a benchmark in Ikarus Scheme:

running stats for (repeat 10000000 (let-values (((x y z) (values 1 2 3))) 'dummy)): no collections 276 ms elapsed cpu time, including 0 ms collecting 277 ms elapsed real time, including 0 ms collecting 0 bytes allocated running stats for (repeat 10000000 (let+ ((x y z) (list 1 2 3)) 'dummy)): 58 collections 211 ms elapsed cpu time, including 42 ms collecting 213 ms elapsed real time, including 43 ms collecting 240016384 bytes allocated

As you see, let+ takes only 211 ms to unpack a list of three elements ten million times; let-values would take 276 ms instead. On the other hand, let+ involves garbage collection (in our example 24 bytes are allocate per cycle, and thats means 240 million of bytes) and depending on the situations and the implementation this may cause a serious slowdown. You may find much better benchmarks than mine in this thread on comp.lang.scheme and you will see that you can get any kind of results. let-values seems to be slow in Ikarus and in Ypsilon with the default optimization options; it can be faster in other implementations, or in the same implementations with different options or in different releases.

However, those are implementation details. The important thing in my view is the conceptual relevance of a language construct. Conceptually I think the introduction of multiple values in Scheme was a mistake, since it added nothing that could not be done better with containers. I think functions should always return a single value, possibly a composite one (a list, a vector, or anything else). Actually, I am even more radical than that and I think that functions should take a single value, as in SML and Haskell.