2022-06-14: Scheme - Multiple Values

At its core, Scheme's evaluation semantics is multiple-value based. Continuations can accept an arbitrary number of values and expressions can yield an arbitrary number of values. This is in contrast to the functional languages ML and Haskell." (Marc Nieper-Wißkirchen, SRFI 210)

History:

R7RS and multiple values

R7RS-small contains the following five constructs dealing with multiple values:

What are "multiple values"? As the name suggests, they are representations of more than one value - but they are bound together. This binding is not the same as some kind of data structure, like a list. Notice the following:

gosh$ (values 1 2)                   <1>
1
2
  1. This creates multiple values from the values 1 and 2.

As a more useful example, (scheme base) contains the procedure exact-integer-sqrt. The definition for this states: "Returns two non-negative exact integers s and r".

gosh$ (exact-integer-sqrt 16)
4
0
gosh$ (exact-integer-sqrt 17)
4
1

How can we get and use all of the returned values?

This is where the define- and let- forms come in.

define-values works like define but matches up a series of identifiers with the respective returned value:

gosh$ (define-values (root remainder) (exact-integer-sqrt 17))
remainder
gosh$ root
4
gosh$ remainder
1

As define-values is matching a list of formals (like lambda), it is easy to get values as lists, etc:

gosh$ (define-values vals (values 1 2 3))
vals
gosh$ vals
(1 2 3)
gosh$ (list? vals)
#t
gosh$ (define-values (head . rest) (values 1 2 3))
rest
gosh$ head
1
gosh$ rest
(2 3)

There are no surprises with let-values and let*-values: these work in the same way as let and let*, but use multiple identifiers to match multiple values:

(define (find-roots x y)
  (let-values (((x-root x-remainder) (exact-integer-sqrt x))  ; <1>
               ((y-root y-remainder) (exact-integer-sqrt y))) 
    (list x-root y-root)))                                    ; <2>
  1. Collect and name the multiple values
  2. Return some in a list

And with let*-values:

(define (roots+total-remainder x y)
 (let*-values (((x-root x-remainder) (exact-integer-sqrt x))
               ((y-root y-remainder) (exact-integer-sqrt y))
               ((roots total-remainder) 
                (values (list x-root y-root)                  ; <1>
                        (+ x-remainder y-remainder))))
    (list roots total-remainder)))
  1. It's a let*- form, so we can access the previous definitions.

The remaining form, call-with-values, ties together the production and consumption of multiple values: a producer is a zero-argument procedure that returns multiple values, using a values expression, and a consumer is a procedure that accepts these values as its arguments.

Notice the little "gotcha" that the producer must be a zero-argument procedure.

(define (show-root n)
  (call-with-values 
    (lambda () (exact-integer-sqrt n))            ; <1>
    (lambda (s r) (display (square s))            ; <2>
                  (display " + ") 
                  (display r) (newline))))

gosh$ (show-root 15)
9 + 6
  1. The producer procedure, returns two values.
  2. The consumer procedure accepts those two values as its arguments.

SRFI extensions

Two SRFIs, SRFI 8 and SRFI 210, are particularly targetted at multiple values: these have both been accepted into the Yellow Edition of R7RS-large.

SRFI 8: Binding to multiple values

This SRFI appears in several other SRFIs, its single definition often copy-pasted into the reference implementation of other libraries.

The SRFI provides the receive syntax, to improve on call-with-values. Specifically, instead of having a zero-argument procedure as producer, receive accepts any expression as a producer, and its values are bound into a set of provided identifiers. These can then be processed within the expression's body.

The show-root example above can be rewritten to use receive as:

(define (show-root n)
  (receive (s r) (exact-integer-sqrt n)           ; <1>
    (display (square s))                          ; <2>
    (display " + ") 
    (display r) (newline)))
  1. The multiple-return values are captured into the given identifiers.
  2. The body can work with those identifiers.

Of course, in a post-R5RS world, we have let-values as an alternative, which also has the advantage of being able to "receive" values from multiple producers. The above example using let-values:

(define (show-root n)
  (let-values (((s r) (exact-integer-sqrt n)))    ; <1>
    (display (square s))                          ; <2>
    (display " + ") 
    (display r) (newline)))
  1. The multiple-return values are captured into the given identifiers.
  2. The body can work with those identifiers.

SRFI 210: Procedures and syntax for multiple values

Aimed at introducing procedures and syntax for dealing with multiple values, such as creating lists and vectors from expressions returning multiple values and procedures returning the elements of a list or vector as multiple values.

This SRFI introduces a number of syntactic forms and procedures. I have divided these into separate groups, with related functionality, but not explained every construct.

Group 1: helpful operations for working with multiple values

[syntax] coarity
Definition

(coarity producer)

Description

Evaluates producer and returns the number of resulting values.

Example
gosh[r7rs.user]$ (coarity 3)
1
gosh[r7rs.user]$ (coarity (values 1 2 3))
3
gosh[r7rs.user]$ (coarity (exact-integer-sqrt 11))
2
[syntax] set!-values
Definition

(set!-values formals producer)

Description

Evaluates producer and assigns its multiple values to identifiers in the formal arguments list. It has the same relation to set! as define-values does to define.

Example
gosh[r7rs.user]$ (define x 2)
x
gosh[r7rs.user]$ (define y 3)
y
gosh[r7rs.user]$ (set!-values (x y) (exact-integer-sqrt 11))
2
gosh[r7rs.user]$ x
3
gosh[r7rs.user]$ y
2
[procedure] value [syntax] value/mv
Definition

(value index value-1 ...) + (value/mv index value-1 ... [producer])

Description

Evaluates one or more values and returns the index position value. For value/mv, the last expression can produce multiple values.

Error

If index is out of range, or a multiple-values producer is in any but the last position.

Example
gosh[r7rs.user]$ (value 1 'a 'b 'c)                 <1>
b
gosh[r7rs.user]$ (value/mv 1 'a 'b 'c)
b
gosh[r7rs.user]$ (value/mv 2 'a 'b (values 'c 'd))  <2> 
c
gosh[r7rs.user]$ (value 2 'a 'b (values 'c 'd))
c
gosh[r7rs.user]$ (value 3 'a 'b (values 'c 'd))     <3>
*** ERROR: argument out of range: 3
gosh[r7rs.user]$ (value/mv 3 'a 'b (values 'c 'd))
d
gosh[r7rs.user]$ (value/mv 1 'a (values 'b 'c) 'd)  <4>
*** ERROR: received more values than expected
  1. Both work with single value expressions to return the indexed value.
  2. Using values can work like a single value.
  3. But the value/mv form can use the values as part of the indexed range.
  4. For value/mv, any producer must be in the last position.
[syntax] case-receive
Definition

(case-receive producer clause-1 ...)

Description

A matching clause is found, matching formals in each clause against the values returned by the producer, as done in lambda. And then the body of that matching clause is evaluated, in the context of any bindings introduced by the match.

Example

We could adapt exact-integer-sqrt so it returns a single value if the remainder is 0:

(define (new-eis n)
  (let-values (((s r) (exact-integer-sqrt n)))
    (if (zero? r)
      s
      (values s r))))

And then write a function to choose how to display the number:

(define (display-root n)
  (case-receive (new-eis n)
    ((s) (display "Exact root: ") (display s) (newline))
    ((s r) (display "Inexact, with remainder: ") (display r) (newline))))

gosh[r7rs.user]$ (display-root 3)
Inexact, with remainder: 2
gosh[r7rs.user]$ (display-root 4)
Exact root: 2
[procedure] identity

The identity procedure is equivalent to values.

Group 2: operations for converting multiple values to and from other data types

[syntax] list/mv
Definition

(list/mv value-1 ... [producer])

Description

Evaluates one or more values and producer, returning all the values as a list.

Error

If a multiple-values producer is in any but the last position.

Example
gosh[r7rs.user]$ (list/mv (values 'a 'b 'c))
(a b c)
gosh[r7rs.user]$ (list/mv 'a 'b 'c)
(a b c)
gosh[r7rs.user]$ (list/mv 'a 'b 'c (values 'd 'e))
(a b c d e)
gosh[r7rs.user]$ (list/mv 'a 'b 'c (values 'd 'e) 'f)
*** ERROR: received more values than expected

There are also natural extensions for vector and (srfi 195) box types, as vector/mv and box/mv.

[procedure] list-values
Definition

(list-values list-items)

Description

Returns the given list of values as multiple values.

Error

If list-items is not a list.

There are also natural extensions for vector and (srfi 195) box types, as vector-values and box-values.

Group 3: helpful operations when calling procedures with multiple values

[syntax] apply/mv
Definition

(apply/mv procedure value-1 ... [producer])

Description

Evaluates one or more values and an optional producer, and then applies the given procedure to the values.

Error

If a multiple-values producer is in any but the last position.

Example
gosh[r7rs.user]$ (apply/mv string #\a #\b)
"ab"
gosh[r7rs.user]$ (apply/mv string #\a #\b (values #\c #\d))
"abcd"
gosh[r7rs.user]$ (string #\a #\b #\c #\d)                   <1>
"abcd"
  1. The previous example is the same as this procedure application.
[syntax] call/mv
Definition

(call/mv consumer producer-1 ...)

Description

Evaluates the procedures, collecting their multiple values together, and then applies the consumer on the values.

Example

We can use this to convert multiple values into a list or vector:

gosh[r7rs.user]$ (call/mv list (exact-integer-sqrt 26))
(5 1)
gosh[r7rs.user]$ (call/mv vector (exact-integer-sqrt 26) (exact-integer-sqrt 3))
#(5 1 1 2)
[procedures] map-values
Definition

(map-values procedure)

Description

Returns a procedure which accepts an arbitrary number of arguments. When applied, it applies the given procedure to each of the arguments, returning the results as multiple values.

Example
gosh[r7rs.user]$ ((map-values exact-integer-sqrt) 3 26)
1
5
[syntax] with-values
Definition

(with-values producer consumer)

Description

The producer and consumer are evaluated, and the procedure resulting from evaluating the consumer is applied to the values results from evaluating the producer.

This is like call-with-values except the producer is simply evaluated, not called.

Example

Rewriting our earlier show-root procedure shows the difference:

(define (show-root n)
  (with-values 
    (exact-integer-sqrt n)                        ; <1>
    (lambda (s r) (display (square s))            ; <2>
                  (display " + ") 
                  (display r) (newline))))

gosh$ (show-root 15)
9 + 6
  1. The producer returns two values.
  2. The consumer procedure accepts those two values as its arguments.

Group 4: manages groups of procedures

[syntax] bind/mv
Definition

(bind/mv producer procedure-1 ...)

Description

Chains a series of procedures together, passing multiple values between them.

Example

bind/mv can be used as a simple form of procedure composition, where multiple values returned from one part are passed on to the next procedure in the line:

gosh[r7rs.user]$ (define (display-pair s r) (display s) (display " + ") (display r) (newline))        <1>
display-pair
gosh[r7rs.user]$ (bind/mv 13 exact-integer-sqrt display-pair)
3 + 4
  1. Define a procedure which takes two values and displays them.

There are also forms specialised for producers of different datatypes:

[procedure] compose-left
Definition

(compose-left procedure-1 ...)

Description

of arguments and return multiple values so the chain is well formed.

Constructs a left-composition of its arguments, so that:

((compose-left f g h) args) #| is equivalent to |# (apply/mv h (apply/mv g (apply f args)))
Examples

compose-left is not much different to normal function composition. For example, we could compose the odd? and not functions together to make a not-odd function:

gosh[r7rs.user]$ (define not-odd (compose-left odd? not))
not-odd
gosh[r7rs.user]$ (not-odd 3)
#f
gosh[r7rs.user]$ (not-odd 4)
#t

The difference is that compose-left internally uses apply/mv to apply the second procedure to the first: hence the first procedure can return multiple values, which the second one can then use. The following example composes the multiple-value returning exact-integer-sqrt with a procedure to display the two values:

gosh[r7rs.user]$ (define (display-pair s r) (display s) (display " + ") (display r) (newline))        <1>
display-pair
gosh[r7rs.user]$ (define display-sqrt (compose-left exact-integer-sqrt display-pair))                 <2>
display-sqrt
gosh[r7rs.user]$ (display-sqrt 3)
1 + 2
  1. Define a procedure which takes two values and displays them.
  2. Now compose this with exact-integer-sqrt, which returns multiple values.
[procedure] compose-right
Definition

(compose-right procedure-1 ...)

Description

of arguments and return multiple values so the chain is well formed.

Constructs a right-composition of its arguments, so that:

((compose-right f g h) args) #| is equivalent to |# (apply/mv f (apply/mv g (apply h args)))
Examples

compose-right is the same as compose-left, but with the order of arguments reversed. The following example composes the multiple-value returning exact-integer-sqrt with a procedure to display the two values:

gosh[r7rs.user]$ (define display-sqrt (compose-right display-pair exact-integer-sqrt)) 
display-sqrt
gosh[r7rs.user]$ (display-sqrt 3)
1 + 2

Inconsistencies across Schemes

Different Schemes handle multiple values in some contexts in different ways.

For example, Gauche permits multiple-values to be used in single-value contexts:

gosh$ (+ (values 1 2) (values 3 4))  <1>
4
gosh$  (map (lambda (a) (values a a)) '(1 2 3))
(1 2 3)
  1. Notice how adding two sets of multiple values together only deals with the first value in each pair of values.

But this is not required behaviour, and indeed the R7RS report states that, for map, it is an error if the mapped procedure does not return a single value.

Chez Scheme gives errors in both cases:

Chez Scheme Version 9.5.8
Copyright 1984-2022 Cisco Systems, Inc.

> (+ (values 1 2) (values 3 4))
Exception: returned two values to single value return context
> (map (lambda (a) (values a a)) '(1 2 3))
Exception: returned two values to single value return context

Kawa only gives an error in the first case, but not the second:

#|kawa:1|# (+ (values 1 2) (values 3 4))
java.lang.ClassCastException: class gnu.mapping.Values$Values2 cannot be cast to class gnu.math.Numeric 
#|kawa:2|# (map (lambda (a) (values a a)) '(1 2 3))
(1 1 2 2 3 3)

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-29.