1

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.

1 Answer 1

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); } 
Sign up to request clarification or add additional context in comments.

4 Comments

Wouldn't this be covered by discussions of tail call optimization? For example: stackoverflow.com/questions/310974/…
@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 and because we're just talking about the philosophies of control flow here.
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?
@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.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.