Return of the Return


Returns

In programming, the primary primitive and abstraction is the function.

You can build an entire model of computation around this idea. See the Lambda Calculus.

Further, we have the paradigm of Functional Programming, based entirely around applying and composing functions.

What do functions actually do?

The essence of a function is to: take an input, apply a computation, and return an output.

output = function(input)

Note this is irrespective of purity, types, side-effects, et cetera.

How do we write functions

In most programming languages, and especially C-like languages, functions will explicitly return a value, with a return statement:

function double(input) {
  result = input * 2;
  return result;
}

Expressions

There is a specific type of programming language called Expression Oriented.

Examples include:

  • Perl
  • Ruby
  • Rust
  • Lisp
  • Julia
  • Haskell

And these are some of my favorites, as i personally find them very expressive to code in.

In these languages most (if not all!) of the language constructs are expressions.

That means that an entire function body can be treated as an expression, and thus the return value of that function.

A simple example in Ruby:

def double(input)
  2 * input
end

Consider a more complicated in Rust (maybe input is a Result type):

//struct SomeError;
fn double(input: Result<i64, SomeError>) -> Result<Option<i64>, SomeError> {
    match input {
        Err(someerror) => Err(someerror),
        Ok(input_val) => match input_val.checked_mul(2) {
            None => Ok(None),
            Some(result) => Ok(Some(result)),
        },
    }
}

Each match is an expression, that reduces to an expression. Even nested matches.

And these can get quite complicated quickly!

Idioms

In many of these expression oriented languages, implicit returns are considered idiomatic. At least Rubocop, the Ruby linter, and Clippy, the Rust linter, will complain about explicit returns.

But i have to insist that:

explicit returns make the code easier to read.

Compare for yourself.

fn double(input: Result<i64, SomeError>) -> Result<Option<i64>, SomeError> {
    match input {
        Err(someerror) => {
            return Err(someerror);
        },
        Ok(input_val) => match input_val.checked_mul(2) {
            None => {
                return Ok(None);
            },
            Some(result) => {
                return Ok(Some(result));
            },
        },
    }
}

Why though?

Maybe it comes down to the extra whitespace and syntax? Maybe the syntax highlighting for the return keyword makes it pop out?

Maybe i’m just a fool that cannot appreciate to optimizations the interpreter or compiler can make? Maybe i’m too procedural and OO brained to appreciate the functional style. Maybe coming from C-like languages has left me with an unshakeable bias?

The control flow always seems easier to follow.

Idioms be damned.

Share: