Closed rbuckton closed 6 years ago
I agree that a static {}
block would be good. At the same time, I worry about the ergonomics of getting rid of nested functions altogether. Having to maintain a list of function variable declarations above the class doesn't feel right to me.
To clarify, I'm not advocating removal of local function
but neither will I advocate for its inclusion. I feel that static {}
is more compelling and that if I have static {}
I am less likely to use or need local function
.
Another thing you can do with a static {}
block is export private behavior to cooperating classes and functions in the outer scope.
Also to clarify on the intended semantics of static {}
, my intuition is the following:
static {}
block creates a new lexical scope (e.g. var
, function
, and block-scoped declarations are local to the static {}
block. This lexical scope is nested within the lexical scope of the class body (granting privileged access to instance private state for the class). static {}
block in its class body. static {}
block is evaluated immediately after all public static field initializers have been evaluated as part of class declaration evaluation, regardless of its order within the class body (this aligns with constructor() {}
).static {}
block may not have decorators (instead you would decorate the class itself). Decorators can always add a class finisher to add their own static initialization.static {}
block's this
receiver is the constructor object of the class (as with static field initializers).I've probably forgotten a few things and may augment this comment as I think of them.
Also a note on static {}
syntax: While I mention a parallel between static {}
for static initialization and constructor() {}
for instance initialization, we cannot say static() {}
as this is already legal javascript for an instance method named "static".
Another thing you can do with a
static {}
block is export private behavior to cooperating classes and functions in the outer scope.
Absolutely:
let A, B;
{
let friend_A_x_get;
let friend_A_x_set;
A = class A {
#x;
static {
friend_A_x_get = a => a.#x;
friend_A_x_set = (a, value) => a.#x = value;
}
}
B = class B {
constructor(a) {
const x = friend_A_x_get(a); // ok
friend_A_x_set(a, value); // ok
}
}
}
(or a hacky use of a static public field initializer)
Example of this hack:
let C;
{
let foo;
let z; // block-scoped "static private" state
C = class C {
#x; // instance private state
static init = (() => { // static privileged initialization
foo = function() { } "static private" behavior
z = new C();
z.#x = ...;
})();
};
delete C.init; // bad because optimizing compilers often de-optimize on delete
}
And one more approach using decorators:
function static_init(member) {
assert(member.placement === "static");
assert(member.kind === "method");
assert(typeof member.key !== "privatename"); // cannot `delete` a private name
const _init = member.descriptor.value;
member.descriptor.value = undefined;
member.finisher = klass => {
_init.call(klass);
delete klass[member.descriptor.key];
};
return member;
}
let C;
{
let foo;
let z; // block-scoped "static private" state
C = class C {
#x; // instance private state
@static_init
static _init() { // static privileged initialization
foo = function() { } "static private" behavior
z = new C();
z.#x = ...;
})();
};
}
While a decorator seems like a fair way to implement this, I'd rather have a syntactic block to better support static analysis in tooling scenarios (such as definite assignment analysis).
NOTE: I've added the following to https://github.com/tc39/proposal-static-class-features/issues/23#issuecomment-360599243, above:
- When evaluated, a
static {}
block'sthis
receiver is the constructor object of the class (as with static field initializers).
I find the parallel between the constructor and the static block quite interesting. Pretending that we don't have field syntax for a moment:
class C {
constructor() {
this.x = 0;
this.y = 0;
}
static {
this.a = 1;
this.b = 1;
}
}
The parallel here is beautiful and makes me far less interested in either static fields or public fields.
When evaluated, a static {} block's this receiver is the constructor object of the class (as with static field initializers).
I think that at this level description, you could say: the static {} block is evaluated as if it was a static concise method of the class that is invoked on the constructor object.
The one hole I see is a reasonable way to synthesize a instance "private method" that has correct super
access. Here is the best I can come up with, for now:
let privateInstanceHelper;
class C extends B {
#priv; //a private instance slot
m() {privateInstanceHelper.call(this)}
static {
let HomeBinder = {
__proto__ = this.prototype.__proto__;
instanceHelper() {
this.#priv; //works when called with a C instance as this.
super.foo() //super call correctly follows C's (original) prototype chain.
}
}
privateInstanceHelper = HomeBinder.instanceHelper;
}
}
If private instance helpers that can correctly do super calls in an common/important enough use case (it isn't clear that it actually is) then perhaps that would partially justify including "private methods" as currently proposed.
@rbuckton I find your argument very compelling. It seems that static blocks are a core primitive that is useful to get at the scope which classes add, and are expressive for many purposes. For example, lexical declarations in class bodies could be explained as syntactic sugar on top of static blocks.
I wonder if, at this point, with difficult tradeoffs every way we turn, it'd be best to start off minimal when it comes to these static class features. That minimal proposal would be static field declarations and static blocks, which give programmers expressiveness, at the cost of some awkwardness, for dealing with the scopes of private names introduced in the Stage 3 class fields proposal. As follow-on proposals, we can consider static private methods and fields, lexical declarations in classes, private name declarations outside of classes, or other things.
Some of the discussion on this thread seems to be about whether other Stage 3 proposals are well-motivated. For private methods and accessors, I responded here. For public static and instance field declarations, you can find the motivation in @jeffmo's excellent explainer (search for "Why").
For details, @rbuckton 's suggestions at https://github.com/tc39/proposal-static-class-features/issues/23#issuecomment-360599243 seem great to me and more sensible than what I was previously imagining. I'd probably go with those. Just for fun, here's some alternatives we might consider:
var
s to be hoisted outside of static blocks (leaning allowing var but not hoisting it, as if it's a separate method definition).@bakkot proposed that static blocks leak their contents to the outside, though this would break the rule that no lexical declarations leak out of curly braces (excluding legacy hoisting constructs).
I don't actually advocate this in this form, to be clear; I'd only be behind it if there were some syntax which didn't have the leaking concern (e.g. if it did not use {
).
The best I could come up with, and it's unacceptably bad, is static:
To clarify, this is just if we wanted to leak declarations. I think the normal static block syntax that @rbuckton proposed, static { }
, is good if we don't need to do that.
Overlapping with label syntax seems… suboptimal.
I've created a more formal proposal for this feature at https://github.com/rbuckton/proposal-class-static-block.
Currently if you want "static private" state and "static private" behavior we are proposing the following design:
While this allows for "static private" behavior to have privileged access to instance private state and "static private" state, the only mechanism we have for privileged initialization of "static private" state is lazy-initialization (or a hacky use of a static public field initializer):
What we need is a mechanism for privileged static initialization:
This leads me to believe that, while
local
functions can give us behavior with privileged access to lexically scoped private names, many scenarios still need a clear mechanism for privileged static initialization.However, if we have privileged static initialization I can instead write my above example as follows:
As such, I feel that privileged static initialization is a better mechanism for providing access to privileged behavior as it aligns both "static private" state and "static private" behavior and reduces overall complexity.