public
Created

  • Download Gist
block-lambda-quandry.js
JavaScript
1 2 3 4 5 6 7 8 9 10
function foo() {
bar({|x|
return 10;
});
}
function bar(block) {
block();
block(); // we should never get here. how?
}
foo();
gistfile1.txt
1 2 3 4 5
10 foo(); // stack: []
2 bar({|x| return 10}) // stack: [foo]
7 block() // stack: [foo, bar]
3 return 10 // stack: [foo, bar, block]

One way is to think of it as the block lambda closing over the current stack, like an implicit lexical binding. So the return says "return to the stack I am closed over."

Another way to think of it is like setjmp and longjmp.

Yet another way to think of it is like throwing a special unique value that's shared only with the lexical contents of foo, so only foo can catch it.

And yet another 'nother way to think of it, which happens to be my favorite, is to treat return as the invocation of an implicitly bound "escape continuation" for the function it's contained within. This is pretty similar to the first way I described it above: the escape continuation is basically like "the current stack." So entering a function binds an escape continuation called return, and calling return jumps back to that continuation.

More about escape continuations: http://www.google.com/search?q=escape+continuations

Dave

@dherman The least terrifying of these, for me, is to think of it like an exception. I was initially confounded because it seems like it would be necessary for the block to interrupt the flow of the higher order function, but with exceptions, there are existing accommodations for these cases. If my function is going to be interrupted, I expect to be able to observe that in a catch or finally clause and clean up resources.

@dherman Perhaps the “exception way of thinking” should be more literally true. A thrown value, perhaps a ReturnValue exception like that codified by your generators improvement proposal, implicitly caught by foo, identified by its object identity as being associated with itself, and converted into a literal return value.

If my function is going to be interrupted, I expect to be able to observe that in a catch or finally clause and clean up resources.

Well first of all, you can use a finally block to clean up resources. That wouldn't change.

As for catch, the difference is that nobody but lexically enclosed code (i.e., you) can fabricate the ability to cause that local exit unless you explicitly pass them a block that you wrote yourself. That is, unlike exceptions, nobody has the ability to cause you to return unless you hand them a capability to return in the form of a block that closes over return.

Perhaps the “exception way of thinking” should be more literally true. A thrown value, perhaps a ReturnValue exception like that codified by your generators improvement proposal, implicitly caught by foo, identified by its object identity as being associated with itself, and converted into a literal return value.

This approach would be dynamic scoping, which has two problems. 1) Any function you call would implicitly have the ability to force you to return; 2) It breaks abstractions; if you pass a block lambda that does a return to another higher-order function, it will accidentally return from that function instead of the function it appeared to be lexically bound to.

@dherman Right. The finally clause might be enough.

I did not mean to imply that you could fabricate a ReturnValue exception and throw it, causing a return from the first parent scope. I mean that a return in a block lambda would construct a ReturnValue exception with a read-only value (at least), add it to a side-table.

var sideTable = new WeakMap();

// return value desugars
var exception = new ReturnValue(value);
sideTable.set(exception, foo);
throw exception;

// foo desugars
function foo() {
    try {
    } catch (x) {
        if (sideTable.get(x) === foo) {
            return x.value;
        } else {
            throw x;
        }
    }
}

Then foo would implicitly catch the exception and convert a ReturnValue.value into a return value only if the exception in the side-table maps to foo.

This would make it possible for an intermediary to observe the exception in a catch block, rethrow it, or throw a different exception. It would not give the ability to return an arbitrary value from an arbitrary parent scope.

@dherman I think that finally will have to suffice. It would be an information/capability leak for an intermediate stack frame to be able to observe the return value in a parent lexical scope. MarkM would kill me for suggesting it.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.