Return Values

Dear Computer

Chapter 4: Functions

Return Values

Just as functions receive data from the caller, they also send data back. The syntactic forms used to return data vary considerably across programming languages. We explore several of these forms.

Return Statements

One common mechanism for returning a value to the called is an explicit return statement, as shown in this Java program:

Java
public static int twice(int x) {
  return x + x;
}

As soon as a return statement is encountered, control is handed back to the caller. Any subsequent code is not executed. In fact, Java compilers reject a program if it has code after a return statement.

Some programmers insist that functions have only a single return statement. This goal is a proxy for the more comprehensive and abstract goal: write code that can be clearly understood by humans. Consider this implementation of a linear search algorithm that uses a single return statement:

Java
public static int findIndex(String[] items, String target) {
  int targetIndex = -1;
  for (int i = 0; targetIndex < 0 && i < items.length; ++i) {
    if (items[i].equals(target)) {
      targetIndex = i;
    }
  }
  return targetIndex;
}

Contrast it with this implementation that uses a second return statement inside the loop:

Java
public static int findIndex(String[] items, String target) {
  for (int i = 0; i < items.length; ++i) {
    if (items[i].equals(target)) {
      return i;
    }
  }
  return -1;
}

Which implementation do you find easier to understand?

Languages allow return statements to be omitted if a function does not produce a value to send back to the caller and it should run all the way to its end, as in this C void function:

Java
public static void log(const char *message) {
  printf("%s\n", message);
}

An explicit return statement is still needed in a void function if its execution must end early.

Other languages allow implicit returns. In these languages, which are sometimes called expression-oriented languages because every construct produces a value, whatever value is produced by the last evaluated expression is returned to the caller. This Ruby function uses implicit returns to send back the number of days in a month:

def day_count(month, year)
  if ["Jan", "Mar", "May", "Jul", "Aug", "Oct", "Dec"].member?(month)
    31
  elsif ["Apr", "Jun", "Sep", "Nov"].member?(month)
    30
    # For fun, add up the numbers of these months.
  elsif is_leap(year)
    29
  else
    28
  end
end

In Rust, a function that returns a value has this grammatical structure: zero or more statements that have a trailing semi-colon, and then an expression with no trailing semi-colon. This function follows this structure:

Rust
fn half_and_half(x: i32, y: i32) -> i32 {
  let half_x = x / 2;
  let half_y = y / 2;
  half_x + half_y
}

The value of the expression is implicitly returned. A trailing semi-colon leads to a compilation error.

Both Ruby and Rust allow explicit returns. An explicit return has extra semantics: the return stops the function from further execution. Implicit returns do not have these semantics; they can only be used when the expression is already the function's final action. Both communities favor implicit returns where possible.

Names

Parameters are usually named, and the names appear in a language's documentation. In a language with only positional parameters, the caller never uses the parameter names, but they are included to describe the function's interface. Return values, on the other hand, are rarely given names in the public interface. Usually only their type is declared, as in this C function that returns an int:

C
int twice(int x) {
  return x + x;
}

In explicitly-typed languages outside of the C family, the type often appears after the parameters, as in this Kotlin function:

Kotlin
fun twice(x: Int): Int {
  return x + x
}

And in this Rust function:

Rust
fun twice(x: i32) -> i32 {
  x + x
}

Matlab programmers, on the other hand, must explicitly name their return values in the function header. In this Matlab function, y is declared as the variable that will hold the returned value at the function's end:

Matlab
function y = twice(x)
y = x + x
end

The assignment syntax in the header reflects that y must be given an assignment somewhere in the function body. Variable y is local to the function. The value assigned to it will be copied back into the caller.

Multiple Return Values

Many languages limit functions to a single return value. Perhaps this is a historical tradition based on assembly languages that reserved a particular register for sending data back to the caller. A few languages do permit multiple return values, or at least give the appearance of them. In this Ruby script, the divmod function gives back both the quotient and remainder of a division:

Ruby
def divmod(a, b)
  [a / b, a % b]
end

quotient, remainder = divmod(20, 3)

puts quotient   # prints 6
puts remainder  # prints 2

Technically, the function gives back a single array value, not multiple return values. The array is then destructured in the caller's assignment statement. Destructuring is independent of a function call; the same multiple assignment can be accomplished with a literal array:

Ruby
quotient, remainder = [6, 2]

Saying that Ruby allows multiple return values would therefore be misleading. The languages that do truly allow multiple return values tend to use out parameters instead of a conventional return value. Out parameters are parameters that are sent in from the caller to be assigned values in the function's body. References in C++ are used to achieve out parameter semantics:

C++
void divmod(int a, int b, int& quotient, int& remainder) {
  quotient = a / b;
  remainder = a % b;
}

int main() {
  int quotient, remainder;
  divmod(20, 3, quotient, remainder);
  std::cout << quotient << " " << remainder << std::endl;  // prints 6 2
  return 0;
}

The quotient and remainder variables are declared in the scope of main and shared with divmod.

C# and the OpenGL shading language (GLSL) annotate out parameters with the keyword out. GLSL also supports in for parameters that may only be read. This GLSL function uses both:

GLSL
void divmod(in int a, in int b, out int quotient, out int remainder) {
  quotient = a / b;
  remainder = a % b;
}

Additionally, GLSL supports inout for parameters that may be both read from and written to:

GLSL
void complement(inout float x) {
  x = 1.0 - x;
}

Plain C++ references are in-out parameters. They can be given in-parameter semantics using the const modifier.

← ParametersPassing Mechanics →