Do you use the return?

A loop is just syntactic sugar for tail recursion.

1 Like

My background has been in the Microsoft development stack, going back to Visual C++ and Visual Basic. When .NET was fairly new, and even for some years after that, I could often look at a C# codebase and tell from the style whether the author had come from Visual Basic or C++, because they hadn't fully adopted C# (or more generally .NET) idioms. Even in a Visual Basic .NET codebase I could detect previous VB Classic idioms vs someone who may have done C++ previously. But it does take time and also some discipline to adjust.

1 Like

I find that an amazing statement. Seems totally backwards to me. A loop like:

loop {
    // Stuff
}

is a simple way of writing in a high-level, platform independent way what the processor does, which in assembler is like:

loop: 
    # Stuff
    jmp loop

On the other hand a high-level language function typically generates a lot more code, it has to create and maintain a stack, push parameters onto it, reserve local variable space on it, maintain it's return address, and so on.

So in my mind tail-recursion is a way to optimise all that away where possible.

Meanwhile, on a more philosophical level a recursive function is a very different thing conceptually to a loop. The canonical example of a recursive Fibonacci implies, at least conceptually, a stack whereas the loop solution does not.

The only reason to leave data and the return address on a stack is if there is extra work to be done after the call returns. The information left on the stack is essentially a closure containing the remaining work: the return address is the function pointer, the other data are the captured variables.

A tail call that doesn't have any work remaining doesn't need to put anything on the stack and can be naturally implemented with a jmp, even when the call is not recursive.

1 Like

Interesting. I never looked at that way before but it makes sense.

I always had this idea the functions in high level languages were invented so as to make code sequences reusable from many different parts of a program. After all, if a code sequence only appears once in a program there is little incentive to make it into a function. As such functions cannot use a jmp for returns, they don't from where they were called so as to jmp back.

As such tail calls are a special optimisation for special uses of a function not syntactic sugar.

Of course we don't need a stack for parameters or local variables if a function does not recuse, directly or indirectly. I once worked with a language that required one to specifically mark functions as "RETETRANT" if they were to recurse. That cause the compiler to do all the extra stack juggling which normal functions did not. Kind of necessary when working on machines with only kilobytes of RAM.

So I thought it was a bit messy to have that start_shell_r() function hanging around that is not supposed to be called from any other place. Why not embedded it into the start_shell() function:

    pub fn start_shell(&mut self) -> Result<()> {
        fn start_shell(chan: &mut Chan, try_num: u8) -> Result<()> {
            match chan.channel.shell() {
                Ok(()) => {
                    chan.shell_open = true;
                    Ok(())
                }
                Err(e) if e.code() == ErrorCode::Session(LIBSSH2_ERROR_TIMEOUT) && try_num < 3 => {
                    start_shell(chan, try_num + 1)
                }
                Err(e) => {
                    error!("Starting shell failed: {e}, try_num: {try_num}");
                    Err(e.into())
                }
            }
        }
        debug!("Creating SSH shell...");
        start_shell(self, 0)
    }

I use where its meant to be used. When you need to return early. Doing it at the end is just redundant.

I agree.

I have sometime heard the argument that sprinkling returns throughout a function obscures the control flow. I think that is true but I also think that if your functions are short and sweet that is only slight and can make the code simpler that trying to force all returns to the end of the function. On the other hand if you functions are long and twisty you have other problems than the returns hiding in it's folds that should be addressed first.

2 Likes

I like that returns make it obvious you're breaking the flow.

One of the big things that, to me, makes early return in Rust feel qualitatively different to early return in C is that Rust code tends not to overload one return type with multiple meanings.

In C, it's common to have a return type like int, where negative values mean errors, zero has one meaning, and positive values have other meanings. In this world, it's quite easy for a typo to turn return 1; into return -1;, or return -EBADF; into return EBADF; with a radical change to the meaning of the code.

In Rust, it's more normal to use a sum type (enum) for this sort of situation, and it's much harder to typo return Ok(1); into return Err(1) or vice-versa; on top of that, you're much more likely to have different types for the happy path and error path variants. That makes early return feel less dangerous, and brings the rule back to "too many early returns is a sign that your function is doing two different things".

7 Likes

OCaml requires that too. 'rec' is their keyword.