tc39 / proposal-static-class-features

The static parts of new class features, in a separate proposal
https://arai-a.github.io/ecma262-compare/?pr=1668
127 stars 27 forks source link

Preference for `static {}` block to perform privileged static initialization #23

Closed rbuckton closed 6 years ago

rbuckton commented 6 years ago

Currently if you want "static private" state and "static private" behavior we are proposing the following design:

let C;
{
  let z; // block-scoped "static private" state
  C = class C {
    #x; // instance private state

    local function foo() { } // "static private" behavior
  };
}

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):

let C;
{
  let z; // block-scoped "static private" state
  C = class C {
    #x; // instance private state

    local function foo() {
      lazyInit();
    }

    local function lazyInit() {
      if (z === undefined) { // lazy "static private" behavior
        z = new C();
        z.#x = ...;
      }
    }
  };
}

What we need is a mechanism for privileged static initialization:

let C;
{
  let z; // block-scoped "static private" state
  C = class C {
    #x; // instance private state

    static { // static privileged initialization
        z = new C();
        z.#x = ...;
    }

    local function foo() { } // "static private" behavior
  };
}

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:

let C;
{
  let foo;
  let z; // block-scoped "static private" state
  C = class C {
    #x; // instance private state

    static { // static privileged initialization
        foo = function() { } "static private" behavior
        z = new C();
        z.#x = ...;
    }
  };
}

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.

zenparsing commented 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.

rbuckton commented 6 years ago

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.

zenparsing commented 6 years ago

Another thing you can do with a static {} block is export private behavior to cooperating classes and functions in the outer scope.

rbuckton commented 6 years ago

Also to clarify on the intended semantics of static {}, my intuition is the following:

I've probably forgotten a few things and may augment this comment as I think of them.

rbuckton commented 6 years ago

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".

rbuckton commented 6 years ago

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
        }
    }
}
rbuckton commented 6 years ago

(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).

rbuckton commented 6 years ago

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's this receiver is the constructor object of the class (as with static field initializers).
zenparsing commented 6 years ago

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.

allenwb commented 6 years ago

see my comment at https://github.com/tc39/proposal-static-class-features/issues/24#issuecomment-360853811

allenwb commented 6 years ago

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.

allenwb commented 6 years ago

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.

littledan commented 6 years ago

@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:

bakkot commented 6 years ago

@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 {).

littledan commented 6 years ago

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.

ljharb commented 6 years ago

Overlapping with label syntax seems… suboptimal.

rbuckton commented 6 years ago

I've created a more formal proposal for this feature at https://github.com/rbuckton/proposal-class-static-block.