Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@isaacs
Created January 15, 2012 00:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save isaacs/1613485 to your computer and use it in GitHub Desktop.
Save isaacs/1613485 to your computer and use it in GitHub Desktop.
// block-lambda proponents: what does this program do?
function omgnomakeitstopkillitwithfire (t) {
x = {|y|
return "returning: " + y
}
// now here comes a bunch of code.
// imagine there are 50 lines here
t(x)
// now a bunch more stuff.
// eventually:
x("there")
// then a bunch more stuff
// ending with:
return 100
}
var ret = omgnomakeitstopkillitwithfire(function (cb) {
alert("before")
cb("here")
alert("after")
})
alert(ret)
/**
* HOW DOES THIS NOT QUALIFY AS INSANE ACTION-AT-A-DISTANCE???
* Seriously, this belongs in the bad-parts bucket with `with` and `eval`.
**/
@isaacs
Copy link
Author

isaacs commented Jan 15, 2012

As I understand the spec proposal, this program would:

  1. enter the aptly named "omgnomakeitstopkillitwithfire", passing the unnamed fn as an argument.
  2. unnamed function gets called with the x lambda as an argument
  3. enter unnamed function
  4. alert "before"
  5. call the x block-lambda, which returns "returning: here" from omgnomakeitstopkillitwithfire, and not run the rest of the function
  6. alert "after"
  7. alert the returned value "returning: here"

This block lambda stuff, I have a very bad foreboding feeling about this. I see myself with a bunch more gray hair, maybe a beard, telling young people not to use it in their programs.

@isaacs
Copy link
Author

isaacs commented Jan 16, 2012

@dherman

Anyway I don't really understand your gist. How is it any more of an issue than a function that throws an exception?

A function that throws an exception breaks execution to the nearest try/catch in the call stack at run-time. So, the caller would have to wrap cb("here") on line 26 in a try/catch block to continue, and it would not affect the return-value of the omgnomakeitstopkillitwithfire function.

That is, there's no way with a function that throws an exception to affect the return value of the caller. The semantics here are:

  1. Give me a function.
  2. At some point in my program, I'll pass that function the capability to end my program, setting the return value.
  3. Before my function has actually returned, however, you can do other actions.

So, it's more like a try { return value } finally { doSomethingElse() }, but less explicit.

And, that's a terrible antipattern anyway, making execution flow unnecessarily difficult to reason about. Saying "it's not any more of an issue than throwing" is not really an argument for block lambdas. It's not any more of an issue than eval or with, either.

@BrendanEich
Copy link

try/finally has its uses, but let's step back a bit.

If you have a big long function and you're having a hard time reasoning about control effects under your authorial control, scoped within that hunk of overlarge code, then refactor. Why isn't this a "doc, it hurts when I do that" scenario?

Can we drop the pointless eval and with references? They don't help your case. No one is saying block-lambdas don't hurt more than them -- many of us believe block-lambdas help and do not hurt.

Arguing with a sketch of overlarge code where the author lost control of block-lambda usage is more helpful but raises some hard questions. Remember, block-lambdas cannot be injected as downward funargs or via heap references to force a return from this overlarge function. Only the function's author can nest (static source relation) a block-lambda whose body contains a return statement that returns from the immediately-enclosing function -- the oversized one in the sketch.

So there's no third party attack. Second parties might be calling this function through a well-defined API, but any block-lambdas they pass in can only return from the caller, not from the overlarge API entry point function. Such a non-local return is like a thrown exception in that it unwinds the overlarge API entry point function's activation (running finallys of course). That's the exception analogy.

There are many ways to obfuscate control flow in a large function. Self-obfuscation is not a real threat among those likely to use block-lambdas. Those learning who do not factor well may burn themselves, but we do not withdraw proposals based on such scenarios. We wouldn't have JS if we had dealt with it this way from day 1!

/be

@grncdr
Copy link

grncdr commented Jan 17, 2012

After looking at this for while, I can't imagine many scenarios where I'd want to have both a block lambda return and a "normal" return in the same function. It seems to me that disallowing mixing of return types in the same (static) scope, would remove at least one obvious way to write confusing code without giving up a lot of expressivity.

In the scenarios where you do want to return "normally" you could use an immediately invoked block like ({|| return <expression>})(). Obviously that's a lot of syntactic overhead compared to return <expression>, but due to being an uncommon use, the explicitness might not be unwelcome.

@BrendanEich
Copy link

@grncdr: stretch your imagination with some Smalltalk-80 code. There are plenty of uses for mixed local and non-local returns. See https://github.com/allenwb/ESnext-experiments/blob/master/ST80collections-exp0-blp.js for a start.

/be

@grncdr
Copy link

grncdr commented Jan 18, 2012

@BrendanEich, much better example of used mixed returns, I retract my previous suggestion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment