tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Symbol.private syntax and proposal resolution #206

Closed Igmat closed 5 years ago

Igmat commented 5 years ago

I see Symbol.private as viable alternative to the way privates presented in existing proposal. Most of you probably knows such position of mine.

Most of the time Symbol.private was a rejected because of:

  1. Issue with Membrane pattern
  2. Lack of branding
  3. Doesn't work the same way as [[InternalSlot]]
  4. Lack of syntax

1 - is solved in #183 2 - could be easily added by one simple decorator, when really needed, in case of Symbol.private. While being implictly added to highly required on its own encapsulation feature (as in existing proposal) leads to unsovable problems like #106, especially for metaprogramming libraries'/frameworks' authors (like me, Vue.js, Aurellia, etc.) 3 - while its questionable why should encapsulated state work in same way as internal slots at all, if it really needed, then solution for this question is easy - lets use Symbol.private instead of [[InternalSlot]] 4 - I'll provide solution for this in this particular thread (I hope it will be final one, since I already provided quite a few)

Unless I misunderstood something in @littledan's list, all major concernes are related to this 4 points. I answered each and every point from @littledan's list, because it is the most complete answer to Symbol.private (few points were unclear to me, so I'm waiting for continuing that discussion). You may join our discussion and see my points here.

Just for history

Lists of already suggested shorthand sytnaxes for Symbol.private. Class only:

  1. 149

  2. 134

  3. https://github.com/tc39/proposal-class-fields/issues/149#issuecomment-442242427
  4. @rdking's class-members

For all symbols:

  1. @zenparsing's Symbol literals
  2. This particular thread will show something very close to Symbol literals, but I got this idea independently and it has few differences

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.

Conclusion

It seems to cover most important ergonomic cases for private at classes. If you see any other important but still not covered cases or have any questions to this syntax feel free to ask/advice/discuss them here - I'm open to futher adjustments and believe that it could be improved in a way that will work for committee (if it's not there yet).

Proposal for futher steps

  1. Move Symbol.private to stage 1
  2. Remove private part of existing class-fields proposal, but leave it in stage 3 and normally proceed it to stage 4 whenever committee decides it's appropriate time
  3. Proceed with shorthand syntax for Symbols (this one, or @zenparsing's, or any other I mentioned before)
    • as a follow-up proposal for symbol-private-proposal
    • as a part of symbol-private-proposal I'm ready to create all needed documents for it (e.g. changes to spec, readme, faq, etc.) and polyfill + transpiler plugin

Also, I want to mention that our small but vocal group (me, @rdking, @shannon) has consensus on it and sees such approach as the best possible way to solve majority of existing problems. Probably @hax and @trusktr also share this position, but I'm not sure.

Does it make any sense to you (@ljhard, @littledan, @erights, @jridgewell, @zenparsing, @bakkot, @syg)?

littledan commented 5 years ago

I'm wondering, how would people feel about discussing private symbols in @jridgewell's repository, and focusing this repository on discussing this class fields proposal?

Igmat commented 5 years ago

First of all, that repo is a fork, so creating issues isn't allowed (or I don't know how to create them?).

And second is that two different types of privacy doesn't have much sense.

littledan commented 5 years ago

Sounds like the lack of ability to make issues is something that should be fixed. Until then, maybe you can discuss things on @zenparsing's repository.

I agree that it'd be overkill to have multiple kinds of private. This repository is pursuing one proposal in that space, whereas that proposal is discussing another kind. We can have issues in both places, with the discussion sorted by which proposal you're working on. In TC39, the champions of each proposal can discuss the pros and cons of each, informed by the discussion in issues in their repository.

Igmat commented 5 years ago

@littledan, I got your point. I obviously could put syntax discussion to that repo. But what is more important in this particular case is my questions regarding status quo of this class-fields proposal. And your thought about steps I proposed. I think that it makes much more sense to discuss them here, don't you agree?

littledan commented 5 years ago

OK, to clarify, I don't plan to pursue your proposal. We had lengthy discussion about whether we should include prefixes like public in field declarations, and decided not to.

Igmat commented 5 years ago

But it's not a field declaration, it's symbol declaration. I proposed shorthand syntax for all kinds of Symbols, anywhere in the code. class example is here just to show how it interacts with classes. public/private keywords used to declare Symbol, not field, and my proposal has no affect on existing class-fields proposal (except I'm asking to move private part from it).

Anyway, syntax discussion could be omitted (or moved to another repo), but action items is still here (e.g. @zenparsing's Symbol literals could be used as well).

So what about answering to this part?

  • Move Symbol.private to stage 1
  • Remove private part of existing class-fields proposal, but leave it in stage 3 and normally proceed it to stage 4 whenever committee decides it's appropriate time
  • Proceed with shorthand syntax for Symbols (this one, or @zenparsing's, or any other I mentioned before)

    • as a follow-up proposal for symbol-private-proposal
    • as a part of symbol-private-proposal I'm ready to create all needed documents for it (e.g. changes to spec, readme, faq, etc.) and polyfill + transpiler plugin
littledan commented 5 years ago

To answer your question, I don't plan to work towards any of those bullet points, including working on a proposal for syntax for symbols. We've discussed the reasons for this at great length in various other issues. Feel free to write a proposal and collaborate with other TC39 delegates and community members to work towards these goals.

trusktr commented 5 years ago

What's really,

really,

really,

really nice

about the proposal of this issue is that it works outside of classes, which makes it much more dynamic (f.e. easy to implement "friends" or other such features using it), and it can still achieve the same as what the "private" feature of class-fields proposal of this repo wants to achieve.

Plus,


@Igmat There's one thing I'm missing in your examples: how does inheritance work with public [#y] = 1? How does the code in a subclass of a class in another module, or the code in a module that imports an object literal that has the public [#y] , access the properties? (I'm also curious about protected inheritance, which would probably be similar.)

Is there a syntax we could use so that names can be accessed as strings, so that it can be even more interoperable with existing libs?

trusktr commented 5 years ago

I don't plan to work towards any of those bullet points

@littledan It'd be awesome if you worked towards considering the pros/cons of what's offered here, rather than simply dismissing it.

trusktr commented 5 years ago

The growth of JavaScript is on an exponential curve it seems, and @littledan your proposal is (IMO) caught right in the cusp (new features like WASM are helping to propel JS even faster): the point prior to which community discussion was limited, and after which growing community involvement has shown that the private proposal in this repo is largely disliked by the community, but maybe not for good reasons (I explained in the other issue about future add-ons).

The right thing to do is to consider the new landscape and new possibilities (like in this issue), give them an honest chance, and delay private properties for a while longer to get the spec just right before it is embedded in stone.

Because we're in the cusp, any slow down from changes to the spec aren't going to drastically delay the release of private fields. Suppose it takes another year or two extra: that's not so bad considering the age of the discussion (at least more than a decade from my limited knowledge).

A lot of people really want this feature, and want it to be robust.

trusktr commented 5 years ago

Random thought: adapting from here and :private and :fooPriv from examples in #205, it could be possible that visibility helpers can be created for POJOs in such a way that allows regular . and [] access:

const priv = Symbol.private()
export const obj = {n:1}

obj:priv.foo = 1
obj:priv['pro'+'perty'] = 1

// Object.keys method is updated to take a second arg, the visibility helper
Object.keys(obj, priv) // ["foo", "property"]

const o2 = { ...obj:priv }
Object.keys(o2) // ["foo", "property"]

const o3 = { ...obj, ...obj:priv }
Object.keys(o2) // ["n", "foo", "property"]
import obj from './obj'
Object.keys(obj) // ["n"]

const priv = Symbol.private()
Object.keys(obj, priv) // []