Remix.run Logo
brucehoult 9 days ago

> For instance, the article itself suggests to use early/premature returns

I like premature returns and think they reduce complexity, but as exclipy writes (I think quoting Ousterhout) 'complexity is defined as "how difficult is it to make changes to it"'.

If premature returns are the only premature exit your language has then they add complexity in that you can't then add code (in just one place) that is always executed before returning.

A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value.

So you don't actually need a distinguished "return" but only a "break" that can be from the main block of the function.

A nice way to do this is the "exit function", especially if the exit function is a first class value and can also exit from called functions. (of course they need to be in a nested scope or have the exit function passed to them somehow).

It is also nice to allow each block to have a "cleanup" section, so that adding an action to happen on every exit doesn't require wrapping the block in another block, but this is just a convenience, not a necessity.

Note that this is quite different to exception handling try / catch / finally (Java terms) though it can be used to implement exception handling.

derf_ 9 days ago | parent | next [-]

> A good language will also have "break" from any block of code, such that the break can also carry a return value, AND the break can be from any number of nested blocks, which would generally mean that blocks can be labelled / named. And also of course that any block can have a return value.

Even in a language that is not "good" by your definition... you have basically just described a function. A wrapper function around a sub-function that has early returns does everything you want. I use this pattern in C all of the time.

brucehoult 9 days ago | parent [-]

It is more inconvenient to make a wrapper function for a function than to make a wrapper block for a block, especially in C where you can't lexically nest functions (not counting GNU extensions).

Naturally all programming languages are equivalent, but some are more convenient than others. See the title of this post "Cognitive load is what matters".

neutronicus 9 days ago | parent | prev | next [-]

An old C/C++ argument lol. The C people want their exit blocks, the C++ people want to write destructors.

As a C++ guy I'm on the early-return side of things, because it communicates quickly which fallible operations (don't) have fallbacks.

You see "return" you know there is no "else".

Also, as a code-formatting bonus, you can chain a bunch of fallible operations without indenting the code halfway across your monitor.

mattmanser 9 days ago | parent | prev | next [-]

Personally I wouldn't agree with this. I've adopted a pattern where I try to only ever return the success value at the end of a function. Early returns of success value don't feel clear to me and make the code hard to read. I think that sort of code should only be used if you need high performance. But for clarity, it hurts.

Instead I think you should generally only use early returns for errors or a null result, then they're fine. Ditto if you're doing a result pattern, and return a result object, as long as the early return is not the success result (return error or validation errors or whatever with the result object).

So I feel code like this is confusing:

    function CalculateStep(value) {
       if(!value) return //fine

       ///a bunch of code
   
       //this early return is bad
       if(value < 200) {
          //a bunch more code
          return [ step1 ]
       }

       ///a bunch more code

       return [ ..steps ]
 
    }
The early return is easy to miss when scanning the code. This is much less confusing:

    function CalculateStep(value) {
       if(!value) return //fine

       ///a bunch of code
   
       let stepsResult : Step[]

       if(value < 200) {
          //a bunch more code
          stepsResult = [ step1 ]
       } else {
          //a bunch more code
          stepsResult = [ ..steps ]
       }

       //In statically typed languages the compiler will spot this and it's an unnecessary check
       if(!stepsResult) throw error

       //cleanup code here

       return stepsResult 
    }
It makes the control flow much more obvious to me. Also, in the 2nd pattern, you can always have your cleanup code after the control block.
lelanthran 9 days ago | parent | prev [-]

I prefer to have both single return and early return, like this: https://github.com/lelanthran/libds/blob/b5289f6437b30139d42...