-
-
Save kriskowal/1691779 to your computer and use it in GitHub Desktop.
function foo() { | |
bar({|x| | |
return 10; | |
}); | |
} | |
function bar(block) { | |
block(); | |
block(); // we should never get here. how? | |
} | |
foo(); |
10 foo(); // stack: [] | |
2 bar({|x| return 10}) // stack: [foo] | |
7 block() // stack: [foo, bar] | |
3 return 10 // stack: [foo, bar, block] | |
… |
@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
orfinally
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 byfoo
, 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.
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
andlongjmp
.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 onlyfoo
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 calledreturn
, and callingreturn
jumps back to that continuation.More about escape continuations: http://www.google.com/search?q=escape+continuations
Dave