Why is early return from functions so important?

Hello, Habr! I present to you the translation of the article “Why should you return early?”

image

By Szymon Krajewski At the beginning of my adventure as a programmer, my code often resembled vermicelli. In any conditional expression, I just did that I immediately went on to describe the correct outcome, leaving the rest to the end. “It works, that’s all,” I told myself, and the code continued to grow, leaps and bounds. Thousands of written methods eventually made me wonder if their internal logic should be changed, returning negative results as early as possible. Thus, I have come to what I now call the “immediate failure” rule.

Obviously, there are several approaches to writing the same function. For example, how can you start the execution of the main part immediately after the positive outcome of the conditional statement, so you can first go over all the negative outcomes, returning errors from the function, and only then go to the main logic. In other words, I discovered different styles of writing conditional constructions.

Two requirements validation approaches


The most basic approach is that if the data meets any conditions, the program directly proceeds to the execution of the main function code. For example, you can send a message only if the variable $emailis a valid address, and $messagenot an empty string. Otherwise, an error will be returned.

function sendEmail(string $email, string $message)
{
    if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
        if ($message !== '') {
            return Mailer::send($email, $message);
        } else {
            throw new InvalidArgumentException('Cannot send an empty message.');
        }
    } else {
        throw new InvalidArgumentException('Email is not valid.');
    }
}

The previous code snippet is exactly what was described earlier. Does this work? Absolutely. Is it readable? Not really.

What is the problem of writing requirements in the code itself?


There are two stylistic problems in the written function sendMail():

  1. There is more than one level of indentation in the function body;
  2. It is impossible to immediately determine the path to the success of a function without fully studying it;

Since the first paragraph deals with the concept of “clean code,” let me focus on the second. Our function has several branches, and the operator is returnnot well reviewed. Of course, the above example is very simple and short, but in practice a huge number of conditions and return values ​​can be observed in the function. At this moment, the hero-savior “the reverse condition ” comes into play .

function sendEmail(string $email, string $message)
{
    if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('Email is not valid.');
    }
    if ($message === '') {
        throw new InvalidArgumentException('Cannot send an empty message.');
    }
    return Mailer::send($email, $message);
}

I understand that I got rid of one elseafter the second conditional statement, but, subjectively speaking, the new code sample looks noticeably cleaner and more aesthetic. It is clear that the “reverse condition” earned the cookie for help ( approx. Translator, also a cookie for him ).

What will I achieve using the “reverse conditions”?


Firstly, the maximum level of indentation in the main body of the function decreased to one, and we also got rid of the nested conditions. The code becomes more readable and easier to understand, and the final instruction at the end of the method becomes more accessible.

Perhaps now you are perplexed, and what exactly I called the approach of "emergency failure". Unfortunately, Jim Shore and Martin Fowler defined this term long before my “Hello world”. Of course, although the goals of both approaches are the same, I decided to rename my vision to "early return." The term describes itself, so I fixed this name "on the sign."

The concept of "early return"


First you need to formally define this term.
“Early return” is the concept of writing functions in such a way that the expected positive result is returned at the end, when the rest of the code should complete its execution as soon as possible if there is a discrepancy with the purpose of the function.
You won’t believe how long I thought about this definition, but in any case, the concept is generalized. What are “expected positive result”, “completion of execution” and “function goal”? I will try to describe this in the future.

Follow the "path to happiness"


“A pot of gold at the end of the rainbow,” do you remember this story? Or maybe you already found such a pot? This is exactly what the “early return” approach can be described. The expected prize, the success of our function is at the end of the path. Looking at the new version sendMail, we can confidently say that the purpose of the function was to send a message to the specified mailing address. This is the "expected positive result", in other words, the "path to happiness." We clearly observe the
image
main path in the function. Having

located the final operation at the very end, we clearly understand what the path to the final goal looks like. One look at the code, and everything falls into place.

Get rid of negative cases as early as possible


Not always the execution of the function leads to the expected result. Sometimes a user may enter an invalid email address in our example, and sometimes an empty string as a message. In both cases, it is required to terminate the function immediately. A function termination can either be a return of a negative value or an error return. This choice is individual. Some people may find it confusing to use multiple return statements, but this is normal practice. In addition, this approach often makes the function more understandable to the human eye.

The “bouncer template” is an earlier described methodology that completes a function in cases of its incorrect state. Take a look at the following example:

function matrixAdd(array $mA, array $mB)
{
    if (! isMatrix($mA)) {
        throw new InvalidArgumentException("First argument is not a valid matrix.");
    }
    if (! isMatrix($mB)) {
        throw new InvalidArgumentException("Second argument is not a valid matrix.");
    }
    if (! hasSameSize($mA, $mB)) {
        throw new InvalidArgumentException("Arrays have not equal size.");
    }
    return array_map(function ($cA, $cB) {
        return array_map(function ($vA, $vB) {
            return $vA + $vB;
        }, $cA, $cB);
    }, $mA, $mB);
}

This code simply tries to make sure that the progress of the function can continue when the internal return constructions include all the basic logic.

Return early from controller actions


Controller actions are an ideal candidate for using the above approach. Actions often include a huge number of checks before they return the expected result. Let's look at an example with updatePostActionin the controller PostController:

/* PostController.php */
public function updatePostAction(Request $request, $postId)
{
    $error = false;
    if ($this->isGranded('POST_EDIT')) {
        $post = $this->repository->get($postId);
        if ($post) {
            $form = $this->createPostUpdateForm();
            $form->handleRequest($post, $request);
            if ($form->isValid()) {
                $this->manager->persist($post);
                $this->manager->flush();
                $message = "Post has been updated.";
            } else {
                $message = "Post validation error.";
                $error = true;
            }
        } else {
            $message = "Post doesn't exist.";
            $error = true;
        }
    } else {
        $message = "Insufficient permissions.";
        $error = true;
    }
    $this->addFlash($message);
    if ($error) {
        $response = new Response("post.update", ['id' => $postId]);
    } else {
        $response = new RedirectResponse("post.index");
    }
    return $response;
}

You may notice that the piece of code is quite voluminous, including many nested conditions. The same passage can be rewritten using the “early return” method.

/* PostController.php */
public function updatePostAction(Request $request, $postId)
{
    $failResponse = new Response("post.update", ['id' => $postId]);
    if (! $this->isGranded('POST_EDIT')) {
        $this->addFlash("Insufficient permissions.");
        return $failResponse;
    }
    $post = $this->repository->get($postId);
    if (! $post) {
        $this->addFlash("Post doesn't exist.");
        return $failResponse;
    }
    $form = $this->createPostUpdateForm();
    $form->handleRequest($post, $request);
    if (! $form->isValid()) {
        $this->addFlash("Post validation error.");
        return $failResponse;
    }
    $this->manager->persist($post);
    $this->manager->flush();
    return new RedirectResponse("post.index");
}

Now, any programmer can clearly see the "path to happiness." Everything is very simple: expect something to go wrong - check it and return a negative result earlier. The above code also has one level of indentation and readability inherent in most of these functions. Again, you no longer need to use elseconditions.

Return early from recursive functions


Recursive functions should also be interrupted as early as possible.

function reverse($string, $acc = '')
{
    if (! $string) {
        return $acc;
    }
    return reverse(substr($string, 1), $string[0] . $acc);
}

The disadvantages of the early return approach


The question arises, is the described concept a panacea? What are the disadvantages?

Problem 1. Code style is a subjective concept.


We programmers often spend more time reading code than writing it. This is a fairly well-known truth. Thus, writing as simple and elegant code as possible is required. I insist on following the concept of "early return" precisely because the main mechanism of the function lies at its very end.

That's just the choice of the style of the code subjective opinion. Some people find it more convenient to use a single return operator, while others do not. The choice, ultimately, is left to every programmer.

image
If you imagine two abstract functions, which one will seem easier to read and understand?

Problem 2. Sometimes “early return” is an unnecessary complication


There are many examples where the “early return” negatively affects the final code. A typical example is setter functions, in which parameters often differ from false:

public function setUrl($url)
{
    if (! $url) {
        return;
    }
    $this->url = $url;
}

This is not the biggest improvement in code readability. The situational use of the approach remains the concern of the programmer himself, because using the standard approach, you can achieve the best result:

public function setUrl($url)
{
    if ($url) {
        $this->url = $url;
    }
}

Problem 3. It all looks like an operator break


Since a large number of operators can be located in a function return, it is not always convenient to understand where the result was obtained from. But as long as you use this approach to complete the incorrect states of the function, everything will be fine. Oh yes, do you write tests?

Problem 4. Sometimes it is better to use one variable instead of many returnoperators


There are code structures in which the “early return” approach does not change anything. Just take a look at the following code examples:

function nextState($currentState, $neighbours)
{
    $nextState = 0;
    if ($currentState === 0 && $neighbours === 3) {
        $nextState = 1;
    } elseif ($currentState === 1 && $neighbours >= 2 && $neighbours <= 3) {
        $nextState = 1;
    }
    return $nextState;
}

The same function with the above concept looks like this:

function nextState($currentState, $neighbours)
{
    if ($currentState === 0 && $neighbours === 3) {
        return 1;
    }
    if ($currentState === 1 && $neighbours >= 2 && $neighbours <= 3) {
        return 1;
    }
    return 0;
}

Obviously, the second example has no big advantages over the first.

Conclusion


How each programmer writes his own code often depends on habits and traditions. It is impossible to determine what is better and what is worse when the final outcome is the same. As they say, in programming, many things are subjective.

The concept of "early return" introduces some rules that are not mandatory for execution in 100% of cases, but which often help to make the code cleaner and more readable. It is very important to maintain discipline when writing code, because following a single code style is more important than choosing it.

Personally, I use the “early return” method as often as possible, especially when urgent termination of functions with incorrect conditions improves the aesthetics of the code. I’m used to the fact that successful completion is always at the end of functions, because it’s not without reason that one of the mantras says:
Follow the path to success and respond in case of mistakes.


Original article
Note: the translator reserves the right to stylistically change phrases from the source.

Also popular now: