block lambda revival, now with semantics
Is this valid?
function Person(a) { this.age = a; } Person.prototype.myage = {|| this.age};
function info(myage) {
console.log('my age is '+myage()); } info(new Person(10).myage); info(new Person(12).myage);
On May 22, 2011, at 10:15 PM, Kam Kasravi wrote:
Is this valid?
function Person(a) { this.age = a; } Person.prototype.myage = {|| this.age};
Block-lambda, per Tennent's Correspondence Principle as cited, uses the same this as if you moved the code inside the {|| ... } outside. This is so expr is equivalent to {|| expr }(). JS hackers do not see function () { ... } so should not expect this to change meaning.
(For this reason among others, block-lambdas have no [[Construct]] internal method. Same as for built-in functions in ES1-5.)
So this is not going to do what you want below:
function info(myage) { console.log('my age is '+myage());
} info(new Person(10).myage); info(new Person(12).myage);
Enclosing the block-lambda within the constructor works:
function Person(a) { this.age = a; this.myage = {|| this.age}; }
Note that in this case, unlike the case with a function expression instead of the block-lambda, the implementation can optimize aggressively: no other this can be bound dynamically in any subsequent call via new Person(10).myage() or myage() in info. This is a stronger guarantee than if Person used
this.myage = function () { return this.age; }.bind(this);
in the absence of aggressive static analysis (without which, who knows what bind means at compile time)?
The full closure pattern works too, of course:
function Person(a) { return {get age() { return a; }, myage: {|| a}}; }
but you have to commit to accessors.
One last note: freezing and joining (see # usage in strawman:arrow_function_syntax referencing strawman:const_functions) do not enable much more optimization in this constructor pattern, however you do it. Whether closing over this or the parameter a, the joined block-lambda (or function) identity cannot join across the Person closure boundary.
(I did not make block-lambdas implicitly frozen and joined since some on TC39 and in the community object to that kind of change without opt-in syntax, and because it doesn't help much, given the change to make this a lexical "upvar".)
So block-lambdas are not going to solve the "bound method" cost problem in JS. For that, you need a class proposal that automatically binds methods to the receiver while at the same time disallowing any kind of dynamic inheritance. ES4 after ActionScript 3 had this; Java etc. do too of course.
Does JS really need it, vs. the prototypal function-valued properties as methods pattern? In any event, nothing block-lambdas or any other this-capturing form can fix by themselves.
On 23/05/2011, at 07:15, Kam Kasravi wrote:
Is this valid?
function Person(a) { this.age = a; } Person.prototype.myage = {|| this.age};
function info(myage) { console.log('my age is '+myage());
} info(new Person(10).myage); info(new Person(12).myage);
If it's valid (I don't know if there can be a block-lambda standing alone outside of a function), it would return the value of the global variable age
, I think, because the enclosing scope's this
in that code above seems to be the global Object.
Brendan, do you really want these block-lambdas to be blocks?
I thought you were just going to borrow its syntax for shorter function(){} semantics, not blocks semantics ?
What if {||} were just the shorter function syntax we want, with the added features we want (completion value as return value, lexical/dynamic this
, ... ?), but not true blocks ?
On May 23, 2011, at 7:06 AM, Jorge wrote:
On 23/05/2011, at 07:15, Kam Kasravi wrote:
Is this valid?
function Person(a) { this.age = a; } Person.prototype.myage = {|| this.age};
function info(myage) { console.log('my age is '+myage());
} info(new Person(10).myage); info(new Person(12).myage);If it's valid (I don't know if there can be a block-lambda standing alone outside of a function), it would return the value of the global variable
age
, I think, because the enclosing scope'sthis
in that code above seems to be the global Object.
Yes, see my reply. The code in between { and } (including {|...| and }, even when used as an expression) can be factored out without names changing meaning.
Brendan, do you really want these block-lambdas to be blocks?
Of course, that's part of the proposal and we've discussed this since at least late 2008. See the link from the Prologue at
I thought you were just going to borrow its syntax for shorter function(){} semantics, not blocks semantics ?
Please read the strawman.
What if {||} were just the shorter function syntax we want, with the added features we want (completion value as return value, lexical/dynamic
this
, ... ?), but not true blocks ?
No, it does not look like a function. It looks like a Ruby block, based on Smalltalk blocks which had different syntax. Different semantics for different syntax.
My expectation was that that this would not be bound in the same manner as this.myage = function() { return this.age }.bind(this); using block lambdas. Thanks for the clarifications below. In the block lambda strawman examples, the surprise is the use of return (and other goto constructs like break). At least in similar discussions related to closures in java, Neal Grafter suggests that return and other control constructs not be allowed (gafter.blogspot.com/2006/08/tennents-correspondence-principle-and.html) due to TCP. I assume this is also why var is being excluded in the strawman. Given the impact to the grammar to exclude var, would this constraint be compile time checked? Would grammar constraints like no var be semantically checked by implementors or folded into the grammar formally? The latter would suggest a production rule other than StatementList*, where VariableStatement would be excluded...
On May 23, 2011, at 9:04 AM, Kam Kasravi wrote:
In the block lambda strawman examples, the surprise is the use of return (and other goto constructs like break). At least in similar discussions related to closures in java, Neal Grafter
(Gafter)
suggests that return and other control constructs not be allowed (gafter.blogspot.com/2006/08/tennents-correspondence-principle-and.html) due to TCP.
That's not quite a solution: you can indeed then factor out the guts of a closure immediately or downward-funarg-ly (sic) invoked -- but you can't factor code the other direction without losing expressiveness at the statement level.
I assume this is also why var is being excluded in the strawman.
No, var is worse because it hoists to top of function body. Strictly different and harder compared to break/continue loop/switch targeting (which falls out for free with proper Completion type usage), and also compared to return (no hoisting).
Given the impact to the grammar to exclude var,
Trivial. Can be done a la the "NoIn" productions but better if we do it with prose. Not a big deal, IMHO.
would this constraint be compile time checked?
Yes, |var| would be forbidden in block-lambdas by early error.
Would grammar constraints like no var be semantically checked by implementors or folded into the grammar formally?
See above; not a big deal, up to specificiers.
The latter would suggest a production rule other than StatementList*, where VariableStatement would be excluded...
That's too duplicative. The NoIn productions are too. We could have parameterized productions, or use prose. This is a sideshow, though.
I don't agree with banning control statements in blocks, in any event.
On Sun, May 22, 2011 at 9:17 PM, Brendan Eich <brendan at mozilla.com> wrote:
Suppose we have a host API to open and close files and we write the following two functions.
var withFile = function(filename, fn) {
var file = openFile(filename);
fn(file);
file.close();
};
var fileContains = function(filename, needle) {
withFile('path/to/file.txt', {|file|
return needle.test(file.getContent()));
});
};
I believe the file will not be closed in this case.
This problem is not introduced because of lambdas. This problem exists already if "fn" throws an error.
Would we be able to do the following to ensure the file is closed if "fn" is either a function that throws or is a lambda that returns?
var withFile = function(filename, fn) {
var file = openFile(filename);
try {
fn(file);
}
finally {
file.close();
}
};
Peter
On Jun 4, 2011, at 7:55 PM, Peter Michaux wrote:
Would we be able to do the following to ensure the file is closed if "fn" is either a function that throws or is a lambda that returns?
var withFile = function(filename, fn) { var file = openFile(filename); try { fn(file); } finally { file.close(); } };
Yes, finally clauses run on the way out due to return. The return from a lambda is like an exception, but not reified as such and not catchable. But finallys run.
strawman:block_lambda_revival
Only a couple of TODOs: 11.1.7.1 [[Call]]
When the [[Call]] internal method for a block-lambda object B is called with a list of arguments, the following steps are taken:
Let funcCtx be the result of establishing a new execution context for function code using the value of B‘s [[FormalParameters]] internal property, the passed arguments List args, and the this value given by the ThisBinding component of the execution context in B‘s [[Context]] internal property. Let result be the result of evaluating the StatementList or EmptyStatement that is the value of B‘s [[Code]] internal property. Exit the execution context funcCtx, restoring the previous execution context. If result.type is normal and result.value is empty then return (normal, undefined, empty). Else if result.type is break, continue, or return and B‘s [[Context]] internal property has already exited, throw a TypeError exception. Else if result.type is break or continue and result.target has already exited, throw a TypeError exception. Else return the Completion value result. TODO: forbid var in block-lambdas (yeah!)
11.2.3 Function Calls
...
TODO: Process Completion return type
The production CallExpression : CallExpression [no LineTerminator here] BlockArguments is evaluated evaluated in exactly the same manner, except that BlockArguments is evaluated instead of Arguments in step 3.
In other words, a block-lambda's [[Call]] internal method must return a Completion in order to attempt to break, continue, or return in the enclosing switch/loop/function. One way to do this: uncatchable internal Completion exceptions for (break, empty, target), (continue, empty, target), and (return, value, empty) thrown by the relevant statements. The uncatchable part is ugly and this looks like an unnecessary change at the statement-grammar level, since Completion type works there already to solve the same problem.
The issue is that within an expression, results are values, not Compietions. There's a bit of magic in the spec by which an exception thrown from sub-expression evaluation is caught by the statement level and packaged up into a (throw, value, empty) completion. Block-lambdas are the first way by which an expression can complete with break, continue, or return.
We could make all expression evaluation in the spec return a union of Reference and Completion types. Explicitness seems best. I'll let this one marinate.
The first TODO proposes to ban var from occurring within block-lambdas. This seems better than contorting the spec to hoist var bindings across block-lambda boundaries. Who is with me? ;-)