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 match
es.
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.