Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

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

Closed
rbuckton opened this issue Jan 25, 2018 · 17 comments
Closed

Comments

@rbuckton
Copy link

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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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

@rbuckton
Copy link
Author

rbuckton commented Jan 25, 2018

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

  • A 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).
  • A class may only have one static {} block in its class body.
  • A 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() {}).
  • A 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.
  • When evaluated, a 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.

@rbuckton
Copy link
Author

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
Copy link
Author

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
Copy link
Author

(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
Copy link
Author

NOTE: I've added the following to #23 (comment), above:

  • When evaluated, a static {} block's this receiver is the constructor object of the class (as with static field initializers).

@zenparsing
Copy link
Member

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
Copy link
Member

allenwb commented Jan 26, 2018

see my comment at #24 (comment)

@allenwb
Copy link
Member

allenwb commented Jan 26, 2018

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
Copy link
Member

allenwb commented Jan 26, 2018

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
Copy link
Member

littledan commented Feb 5, 2018

@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 #23 (comment) 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:

  • Rather than static public field scope, static blocks could execute in computed property name scope (as proposed here)
  • @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).
  • Rather than a single static block evaluated at the end, there could be multiple static blocks in the class, which are interspersed in their execution with static public fields rather than run at the end.
  • We could let class decorators see static blocks as anonymous static public field declarations and even allow them to be individually decorated, why not.
  • I'm not sure whether we should allow vars to be hoisted outside of static blocks (leaning allowing var but not hoisting it, as if it's a separate method definition).

@bakkot
Copy link

bakkot commented Feb 5, 2018

@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
Copy link
Member

littledan commented Feb 5, 2018

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
Copy link
Member

ljharb commented Feb 5, 2018

Overlapping with label syntax seems… suboptimal.

@rbuckton
Copy link
Author

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants