jridgewell / proposal-private-symbols

A proposal for private symbols in JavaScript
5 stars 1 forks source link

Symbol shorthand syntax as reification step #1

Open Igmat opened 5 years ago

Igmat commented 5 years ago

Originally I posted it in https://github.com/tc39/proposal-class-fields/issues/206, but it seems that class-fields were improper place for such proposals. @jridgewell, I've checked your presentation and it seems that following proposal could be used to adjust your Symbol.private approach. What do you think about that?

Syntax

Declaration

Use public, private (protected as follow-up) as declaration keyword, like var/let/const.

let x; // variable declaration
public #x; // create `Symbol()` and store it in lexically scoped constant `#x`
private #y; // create `Symbol.private()` and store it in lexically scoped constant `#y`

Assigment

Any assignment without receiver leads to early SyntaxError

public #x = 1; // SyntaxError
public #y = Symbol(); // SyntaxError
public [#z] = [Symbol()]; // SyntaxError
#k = 1; // SyntaxError
#l = Symbol(); // SyntaxError
[#m] = Symbol(); // SyntaxError

Proper assignment always has receiver, [] and has no keyword.

public #x;
obj[#x] = 1;

Declaration + computed property syntax in object literals

The true power comes with computed property syntax.

public #x;
existingObj[#y] = 1; // throws because #y isn't declared yet
const obj = {
    [#x]: 1,
    public [#y]: 1,
    private [#z]: 1,
};
const otherObj = {
    private [#x]: 1, // shadows `#x` from outer lexical scope
    [#y]: 1, //  throws because #y isn't declared yet in this scope
    private [#z]: 1,
};

There is another idea to closure scope instead of lexical one - in this case private [#x] in otherObj will throw, since redeclaration of #x is restricted. Both ways make some use-cases easier and others - more complex, but provide same feature set, so it's a discussible question which one to prefer.

Declaration + computed property syntax in classes

Work mostly the same way as for objects.

public #x;
existingObj[#y] = 1; // throws because #y isn't declared yet
class SomeClass {
    [#x] = 1;
    public [#y] = 1;
    private [#z] = 1;
};
class SomeOtherClass = {
    private [#x] = 1; // shadowed `#x` not shared with `SomeClass`
    public [#y] = 1; // another `#y` not shared with `SomeClass`
    private [#z] = 1; // another `#z` not shared with `SomeClass`
};

Simple mental model

# stands for Symbol. Any variable starting with this sign is ALWAYS Symbol. So code like this private #x should be read as private symbol x.

Discussible moments

I used already reserved keywords, since we are safe to use them + they are good fit for such mental model. But, obviously, we could select some others, for example:

let x; // variable declaration
sym #x; // create `Symbol()` and store it in closure scoped constant `#x`
psym #y; // create `Symbol.private()` and store it in closure scoped constant `#y`
// or even
#sym #y;  // create `Symbol.private()` and store it in closure scoped constant `#y`

Possible follow-up proposals

  1. Symbol.protected/Symbol.friend/Symbol.<whatever> and <whatever> #x declaration syntax;
  2. <whatever> #x for 'key' as shorthand for const x = Symbol.for('key');
  3. Probably some others, not discovered yet.
trusktr commented 5 years ago

I think class-fields is a great place for the proposal, after all, many people dislike the proposal, and what better place to suggest alternatives than there? If the idea turns out to be good, then it would be even fine to update that repo if not creating a new repo.

Placing it here may not get the idea as much exposure unfortunately.

ljharb commented 5 years ago

@trusktr make a new repo for your own proposal; it's up to the champions what they want to entertain in a proposal's repo. If the idea turns out to be good, it is up to the champions to incorporate it into their proposal, or, it's up to the committee to advance the new proposal.

Igmat commented 5 years ago

I revisited a little bit my approach, and found out that there are no major reasons to not make it more class-fileds compatible, so to adjust initial proposal a little bit we can change this rule:

Proper assignment always has receiver, [] and has no keyword.

To "Proper assignment always has receiver and has no keyword." This leads to following additions (but not replacement, everything shown above is still valid syntax):

public #x;
obj.#x = 1;
public #x;
existingObj.#y = 1; // throws because #y isn't declared yet
const obj = {
    #x: 1,
    public #y: 1,
    private #z: 1,
};
const otherObj = {
    private #x: 1, // shadows `#x` from outer lexical scope
    #y: 1, //  throws because #y isn't declared yet in this scope
    private #z: 1,
};
public #x;
existingObj.#y = 1; // throws because #y isn't declared yet
class SomeClass {
    #x = 1;
    public #y = 1;
    private #z = 1;
};
class SomeOtherClass = {
    private #x = 1; // shadowed `#x` not shared with `SomeClass`
    public #y = 1; // another `#y` not shared with `SomeClass`
    private #z = 1; // another `#z` not shared with `SomeClass`
};

So the only major difference comparing to your existing proposal @jridgewell, is need for the keyword. Does it make sense to you?

P.S.

I've just got an idea how to add private #x for 'key', what would it mean and how to add reasonable protected that actually will protect something. @ljharb, I remember that you had concerns about this keyword. Does it make sense to describe it, @jridgewell, @ljharb? Or am I going too far with this shorthand syntax proposal?

jridgewell commented 5 years ago

So, I've already thought of almost exactly this, but I was limiting it to private:

private #x;  // #x is a constant binding.
obj.#x  // dot access

Except, I was going to allow a PrimaryExpression (bare) form:

obj[#x]  // Same as `obj.#x`

// Share the #x reified private symbol
someFunction(#x)  // #x is just CallExpression argument,
                  // providing the reified private symbol

This was to support dynamic access usecases.

Outer scoped private

@bakkot mentioned to me in the last meeting a desire to make all private declarations include the private keyword:

class Ex {
  private #x = 1;
}

This was specifically to allow sharing an outer scoped private in inner scopes, and to support top-level private eventually:

private #x;
private #y;

class Ex {
  #x = 1;  // Reuses outer #x
  private #y;  // Declares new #y
}

Overall, I like this idea. But, it needs to be discussed in the context of class-fields, not just private symbols. I won't be adding this to the private symbols proposal unless class-fields adopts it first. Adding more changes compared to the class-fields proposal gives the committee more things to complain about, and I'm already fighting an uphill battle.

Public keyword

I'm a little hesitant to add # support for public symbols. Having this.#x always mean "private x" makes code easier to reason about. But I'm not against the idea entirely.

Provided the committee decides to go with private symbols, I'd be happy to work on this as a follow-on proposal to see what the committee thinks.

Protected keyword

Protected (as it behaves in other languages) just isn't going to happen in JS. The only way to do it is adding access-guard checks to all property lookups, which the implementers are going to reject outright.

But, what most people want with protected is just a shared private symbol. I think this is the path forward, not a protected keyword.

Igmat commented 5 years ago

@jridgewell, you can't even imagine how happy am I to hear that you're working in this direction. If you need any help (docs, readmes, faq, transpilers, tests or whatever else), feel free to contact me (here or directly ichulinda@gmail.com / ichulinda@dataart.com) - I'll be glad to help you.

Except, I was going to allow a PrimaryExpression (bare) form:

I didn't mention it explicitly, but my proposal includes #x bare form for reified key.

I'm a little hesitant to add # support for public symbols.

Ok, I understand that. We may exclude it for now.

Protected (as it behaves in other languages) just isn't going to happen in JS.

I didn't even thought that it has to provide exactly same way as in other languages. My idea is related to scope, especially for for('x') symbol API. Briefly, it should look like:

  1. public for - global scope (as it works now)
  2. protected for - module scope
  3. private for - lexical or closure scope

But let's keep it aside - it indeed could be follow-up, that we may dicuss later.

Overall, I like this idea. But, it needs to be discussed in the context of class-fields, not just private symbols. I won't be adding this to the private symbols proposal unless class-fields adopts it first.

Ok, I understand that, but taking @littledan's feedback in https://github.com/tc39/proposal-class-fields/issues/206#issuecomment-452382774 and https://github.com/tc39/proposal-class-fields/issues/183#issuecomment-452425068 into account it seems that there are no proper place to discuss something that relates to both class-fields and private symbol. Obviously, you as committee members may have some other communication channels, but it would be great if we (I mean community) have an option to follow what is going on on this field, at least in read-only format. @littledan, @jridgewell what do you think about that?

From my side, I'm very interested in Symbol.private evolving and would love to help. For instance I may describe new syntax (for private only) in more details, and create separate documents for possible follow-ups (public, protected and for) with high-level overview. What would you prefer from the following list?

  1. Separate issue for each proposal
  2. PR for each proposal to this repo
  3. Separate repo for each proposal

Also, since I don't see how the process works inside, my assumptions about which activities are most valuable could be wrong, so feel free to point the correct direction, which will most likely lead to Symbol.private acceptance.

P.S.

I don't know for sure, but I guess that @shannon, @rdking and @hax could be interested in this too.

jridgewell commented 5 years ago

I didn't mention it explicitly, but my proposal includes #x bare form for reified key.

Sweet. I think I got confused when you said "proper assignment always has receiver, [] and has no keyword", but now I realize you were specifically talking about object assignment.

taking @littledan's feedback into account it seems that there are no proper place to discuss something that relates to both class-fields and private symbol.

I read his comments as "I don't want to work on private symbols", not that he doesn't want to discuss things that affect them both. Changing #x = 1 to private #x = 1 in class-fields is a specific request that actually applies to the class-fields proposal.

I know the committee members have privately discussed it, but I don't see it anywhere on the class-fields proposal. There was a strawman proposal to use an outer keyword:

class Outside {
  #x = 1;
  #y = 2;

  constructor() {

    class Ex {
      outer #x = 1;  // Reuses outer #x
      #y = 3;  // Declares new #2 
    }

  }
}

I like using private keyword to declare a new lexical private more than the strawman's outer (@bakkot does, too, this is what we talked about). We should bring this up so there's an actual issue on it. Similar issues: https://github.com/tc39/proposal-class-fields/issues/71, https://github.com/tc39/proposal-class-fields/issues/111, https://github.com/tc39/proposal-class-fields/issues/126

What would you prefer from the following list?

Separate repos please. Or, it can be your own fork of this repo. I just don't want to confuse people who visit this repo, I intend to keep this one as minimal as possible so it's easier to get through committee.

rdking commented 5 years ago

@jridgewell

But, what most people want with protected is just a shared private symbol. I think this is the path forward, not a protected keyword.

I'd wager what most people want is both. Familiarity with the meaning of existing keywords helps speed along understanding. So a declaration like protected #foo = 2 would be pretty clear to most that the symbol #foo would be available in a derived class after they get used to # === Symbol.