11
$\begingroup$

(inspired by this question)

Different programming languages handle multiple return values differently, used in functions such as divmod.

For example, in Python or Haskell, you would just return a tuple. In Go or Lua, you would return multiple values - not a specific type.

What are the different ways of handling multiple return values, and what are the pros and cons of each?

$\endgroup$
0

7 Answers 7

11
$\begingroup$

Push them onto the stack

This really only applies to stack-based languages, and perhaps Assembly. The good thing about this approach is that its somewhat more natural to work with the returned values. If you do divmod, you can pop once to get the quotient, pop the next element to get the remainder. The downside is that if you want to call a function that returns multiple values and immediately pass them on, it might be a bit cumbersome, depending on the language.

$\endgroup$
1
  • 2
    $\begingroup$ Worth noting that this is Lua's solution too, despite not being a stack-based language. $\endgroup$
    – ATaco
    May 26, 2023 at 3:06
10
$\begingroup$

Custom types

In languages such as Java and Kotlin, which really love their nominal typing, you would make a new class specific to that one function. For example, divmod might return a DivmodResult object with fields quotient and remainder (Kotlin does have a Pair class in its standard library, but it's dropped actual tuples now).

Advantages:

  • When you have to return 4 or more values, it really helps to have a class specifically for that purpose if you have a statically typed language. I'd rather see a return type of FooResult than some enormous tuple like (int, String, BarProducer, String, int)

Disadvantages:

  • Making a whole class for a single function is annoying. Some languages do have syntactic sugar for this (e.g. data class in Kotlin) but it's so much easier to use something like tuples or lists instead. In Java, you'll have to make getters and setters and constructors for a minor class that might only be used in one or two places.
$\endgroup$
3
  • 1
    $\begingroup$ Note that Pair and Triple exist in Kotlin’s stdlib, and in 3rd party Java libraries $\endgroup$
    – Seggan
    May 19, 2023 at 2:03
  • $\begingroup$ @Seggan Yes, but that's only for 2/3 values, and Java code conventions encourage making your own classes afaik $\endgroup$
    – user
    May 19, 2023 at 2:04
  • 1
    $\begingroup$ Beyond 3 values is where you’d want to make a user defined class anyway $\endgroup$
    – Seggan
    May 19, 2023 at 2:13
10
$\begingroup$

Structured Bindings

A lot of languages have a feature where you can unpack the return value of a function into multiple individual variables. For instance, it looks like this in C++:

auto [q, r] = divmod(26,7);

divmod in this case would return a value similar to some of the other answers (such as a custom object or builtin tuple type). The nice thing about structured bindings is that they provide a nice syntax for something that is generally tedious. For instance, if divmod here returned a pair of values, then the code above is really syntactic sugar for the following:

auto ret = divmod(26,7);
auto q = ret.first;
auto r = ret.second;
$\endgroup$
6
$\begingroup$

Pointer Parameters

Languages that have pointers or references accept, as parameters, pointers to output variables. Example:

void sincos(float t, float *s, float *c) {
    *s = sin(t);
    *c = cos(t);
}

The main advantage is that this is flexible and simple to implement in languages such as C, but handling be slower than handling a return value directly.

$\endgroup$
2
  • 1
    $\begingroup$ It may also be somewhat less safe, since you don't have a guarantee that the pointer was actually set to something other than garbage/NULL. $\endgroup$
    – user
    May 19, 2023 at 2:00
  • 6
    $\begingroup$ More generally, these are referred to as "output parameters" or "out-parameters". It's possible for a language to support them with syntax like void sincos(float t, out float s, out float c) without the language necessarily supporting pointers. $\endgroup$
    – kaya3
    May 19, 2023 at 3:26
6
$\begingroup$

Generators

Some modern languages like Python and JavaScript provide generator functions, which return an object that yields all return values in succession (an iterator object). It has much simpler usage than defining an object/custom type. A JavaScript example would be as follows:

const foo = function*() {
  yield 'a';
  yield 'b';
  yield 'c';
};

These are useful as they can be directly used in loops or just collected into an array for future use.

$\endgroup$
2
  • 2
    $\begingroup$ I think generators are not used to return multiple return values but rather return multiple times where each iteration can be computed as needed. When you want to return multiple things in Python or JavaScript you can just return a tuple/array. Using generators instead of tuples is just making things more complicated for no good reason. $\endgroup$
    – Aiono
    Jul 6, 2023 at 13:07
  • $\begingroup$ Generators is a very valid option when the "multiple return values" are array/list-like. $\endgroup$
    – Pablo H
    Aug 23, 2023 at 18:27
3
$\begingroup$

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.

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

$\endgroup$
1
$\begingroup$

Redirections

Good insights could be found in pipe redirections in a shell script.

{ { tee >(xxd >&3) >(wc -c >&4) | ...; } 3>&1 | ...; } 4>&1 | ...

Unfortunately, if we want to read the values in the same command, it either needs some dirty hacks, or needs named variables or named pipes, in which case it might be better to just use normal named variables. But we get the idea.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .