0

Let's say, I've two functions f1() and f2(). To serve its purpose, f1() needs to return a truthy value. f1() calls f2() and based on conditions, I want f2() to either return a truthy value or return directly from f1() (the calling function).

//f1()
function f1() {
    const a = f2();
    ...
    ...
    return ...
}

//f2()
function f2() {
    ...
    if(error) {
        // I want to execute return on f1()'s scope here
    }
    ...
    return ...
}

My Approach:

//f2()
function f2(callback: () => void) {
    ...
    if(error) {
        callback();
    }
    ...
    return ...
}

//f1()
function f1() {
    const a = f2(() => {return...});
    ...
    ...
    return ...
}

Unfortunately, it's only returning back the values in the f2() as expected.

How to solve this issue? Please, don't suggest other things like returning null from f2() in if block and return in f1() based on condition. I want f2() to end execution of f1() because of my own reasons.

Thanks.

user311413
  • 137
  • 1
  • 8

1 Answers1

0

f2 doesn't necessarily know it's being called from f1, right? You could refactor someday to have f1 call f3 which then calls f2. At that point it would be unclear whether you want f2 to force the return of f3 or f1. In general, languages expect that f2 is written not to know exactly where it is being called, or to have deep control over what happens in the calling method: f2's return value should substitute for the call to f2 without having further control over f1's execution.

The typical ways of solving this problem are to throw an exception (which will go up the stack to the point where it is caught, and would be especially reasonable given this seems to be an error case) or to return a sentinel value that indicates that you should return early. It sounds like falsy values like false or null are good candidates, but you've already indicated that this standard approach is unsuitable for your needs.

However, another technique you could use is a continuation passing style, which I suggest because it seems you're fine to edit f2 to take a callback (of which a continuation is a special form). This allows f1 to delegate control to f2, except that it calls the callback on success rather than failure. This is much more common in asynchronous cases like Promises than in your synchronous case, but it might allow for the flexibility you need in the synchronous case, and it does allow you to delay the call of continuation if f2 becomes asynchronous. It also allows f1 to choose whether or not to return immediately, and allows for a hypothetical refactored f3 to accept and pass along f1's continuation or to pass its own (potentially invoking f1's continuation after further processing).

function f1() {
  return f2(a => {
    /* the rest of f1 that consumes a */
    return ...
  });
}

function f2(continuation: (a: TypeOfA) => typeOfF1) {
  if (error) {
    // f1 returns this immediately, and never has its continuation called.
    return ERROR_VALUE;
  }
  // calculate return value
  return continuation(a);
}
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • Wouldn't this be covered by discussions of tail call optimization? For example: https://stackoverflow.com/questions/310974/what-is-tail-call-optimization – Thomas Bitonti Apr 17 '20 at 18:57
  • @ThomasBitonti Tail call optimization describes how to implement this structure well without overflowing your stack, sure, but I didn't get the sense that this particular question was sufficiently recursive to require it. I deliberately left out performance concerns because [it's complicated in Javascript right now](https://stackoverflow.com/q/37224520/1426891) and because we're just talking about the philosophies of control flow here. – Jeff Bowman Apr 17 '20 at 19:12
  • The goal seemed to be, in the calling sequence `f0` calls `f1` calls `f2`, to have `f2` return to `f0`, bypassing the return from `f1`. Isn't that the basic idea of tail call optimization? – Thomas Bitonti Apr 20 '20 at 02:48
  • @ThomasBitonti No and no. First, the goal is not just to let `f2` return to `f0`, it's that `f2` *cancels the rest of `f1`*, which also implies that the call to `f2` is not in a tail position of `f1` (or else there wouldn't be any behavior to cancel). Second, Tail Call Optimization isn't about bypassing behavior, it's just an implementation detail that better allows you to _pretend the computer has infinite memory_ by saying "if the only thing `f1` has left to do is return this `f2` result, let's save memory by forgetting about the call to `f1`. *If `f1` has anything left to do, it's not TCO.* – Jeff Bowman Apr 20 '20 at 07:59