Skip to main content
added 441 characters in body
Source Link
coredump
  • 953
  • 3
  • 7

Also, knowing that you only expect N values from the caller could be used to avoid computing the remaining M-N values (M>N) in the called function, in particular if you only ever need the primary return value you could avoid computing them in the called function: this is not possible here I guess because you can have side-effects when computing secondary values (but often, secondary values are bonus values you had to compute anyway).

Also, knowing that you only expect N values from the caller could be used to avoid computing the remaining M-N values (M>N) in the called function, in particular if you only ever need the primary return value you could avoid computing them in the called function: this is not possible here I guess because you can have side-effects when computing secondary values (but often, secondary values are bonus values you had to compute anyway).

Source Link
coredump
  • 953
  • 3
  • 7

Common Lisp

You can return zero, one or more values, using VALUES:

(values 1 2 3)

For example you can also return no value with:

(values)

The first value is the primary return value, and without capturing the secondary values explicitly, this is the only one that is used by other expressions:

(* (values 1 5 7) 2)
== (* 1 2)
== 2

An expression can explicitly ask for multiple values:

(multiple-value-bind (q r) (floor 13 2)
  (format t "13 is ~d * 2 + ~d" q r))

The above prints:

13 is 6 * 2 + 1

You can bind less variables than there are values:

(multiple-value-bind (q) (floor 13 2) ...)

Or more:

(multiple-value-bind (q r s) (floor 13 2) ...)

In which case s will be nil.

The multiple value mechanism is constrained by MULTIPLE-VALUES-LIMIT which is allowed to be as low as 20.

In my current SBCL environment, this value is larger:

USER> multiple-values-limit
4611686018427387903

Implementation

Multiple values as defined here are interesting because it offers a different way to pass data than using objects or lists. It behaves like optional function parameters but for return values: the caller does not need to unpack values to get the primary value, so you can transparently pass additional return values without breaking existing code (like you can add optional parameters without breaking existing code).

Also, you can grab all the values as a list, using MULTIPLE-VALUE-LIST, and convert a list to multiple values with VALUES-LIST, like you can bind function arguments with &rest and use them with apply.

In that regard values are dual to function arguments.

Multiple values were defined to allow better compilation mechanisms that just relying on existing sequences: maybe a smart-enough compiler can pass a temporary list through registers, but having a dedicated mechanism for it makes it easier to implement.

In a Lisp that compiles code, like SBCL for x86, multiple values use registers, flags and possible stack space: the first three return values are in edx/edi/esi registers, the remaining on the stack (rbp), the carry flag (stc/clc) indicates if there are multiple values, and ecx stores how many return values there are (this is a guess based on the disassembly output).

For example:

USER> (disassemble (lambda () (values 10 20)))
; disassembly for (LAMBDA ())
; Size: 40 bytes. Origin: #x54EF67FC                          ; (LAMBDA ())
; 7FC:       498B4510         MOV RAX, [R13+16]               ; thread.binding-stack-pointer
; 800:       488945F8         MOV [RBP-8], RAX
; 804:       BA14000000       MOV EDX, 20
; 809:       BF28000000       MOV EDI, 40
; 80E:       488D5D10         LEA RBX, [RBP+16]
; 812:       B904000000       MOV ECX, 4
; 817:       BE17010050       MOV ESI, #x50000117             ; NIL
; 81C:       F9               STC
; 81D:       488BE5           MOV RSP, RBP
; 820:       5D               POP RBP
; 821:       C3               RET
; 822:       CC10             INT3 16                         ; Invalid argument count trap
NIL

Above, 20 and 40 are respectively the representation of 10 and 20, because there is a shift of one binary digit to the left due to values being tagged.

The primary value 10 is stored in edx, the second one in edi. With more parameters for values they are stored relatively to rpb:

; 84:       BA14000000       MOV EDX, 20
; 89:       BF28000000       MOV EDI, 40
; 8E:       BE3C000000       MOV ESI, 60
; 93:       48C745F050000000 MOV QWORD PTR [RBP-16], 80
; 9B:       48C745E864000000 MOV QWORD PTR [RBP-24], 100
; A3:       48C745E078000000 MOV QWORD PTR [RBP-32], 120

Most functions however only use a single return value, for which it only needs to store the return value in edx and clear the carry flag (clc):

USER> (disassemble (lambda () 10))
; disassembly for (LAMBDA ())
; Size: 21 bytes. Origin: #x54EE17BC                          ; (LAMBDA ())
; BC:       498B4510         MOV RAX, [R13+16]                ; thread.binding-stack-pointer
; C0:       488945F8         MOV [RBP-8], RAX
; C4:       BA14000000       MOV EDX, 20
; C9:       488BE5           MOV RSP, RBP
; CC:       F8               CLC
; CD:       5D               POP RBP
; CE:       C3               RET
; CF:       CC10             INT3 16                          ; Invalid argument count trap
NIL

Disadvantages

A drawback of this representation of multiple values for me is that they are positional, and you have to take care of which secondary value is more important than another otherwise you keep having to bind values you don't care. Here for example, get-decoded-time returns 9 values: second, minute, hour, date, month, year, day of week, daylight-saving flag and timezone.

If I don't care about seconds, I have to ignore it (otherwise there is a warning about an unused variable):

(multiple-value-bind (s m h) (get-decoded-time)
  (declare (ignore s))
  (format t "~a:~a" h m))

I said above that values mirror function calls, but function calls also accepts &key parameters, which are named parameters, but there is no equivalent for values. The below code, for example, doesn't work:

(multiple-value-bind (&key hour minute) (get-decoded-time)
  (format t "~a:~a" hour minute))

Another problem is that its a bit verbose, but some things are like that in Common Lisp, that's not a problem of the approach by itself which could be adapted to have a shorter syntax.