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

[Open discussion] What would be for me the perfect class in js #15

Closed lifaon74 closed 6 years ago

lifaon74 commented 7 years ago

Hello everybody, after following the private class fied proposal and now this proposal, I wanted to discuss : why we don't go further.

First I need to specify i'm not a java, C++ guy or whatever, but a really involved an ecmascript lover. My motivation is to reach a consistent structure across languages heavily used and which have already implemented all the best for classes (since decades).

So for me what would be the perfect class in ecmascript :

1) attribute/method modifiers

In most of the language we find : public, private, protected and static. Currently only static is supported. For me we should use all of this words (already implemented in many languages) to keep consistency and fast code adaptation from one language to another.

The # for private sound wrong for me and the discussion (https://github.com/tc39/proposal-private-fields/issues/14) didn't convince me (people proposed concrete solutions to every problems...). Moreover the protected is still missing but extremely used in inheritance (sound strongly necessary).

Because we love to have full control of class properties in ecmascript, we could add a new attribute to descriptor when using Object.getOwnPropertyDescriptor or Object.defineProperty(https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Object/getOwnPropertyDescriptor) :

interface Descriptor {
  value:? any;
  writable:? boolean;
  ...
  modifiers: ('static' | 'public' | 'protected' | 'private')[]
}

The new modifiers attribute could be an array of string representing the access of the method, and could potentially be modified after initialisation (to allow the same power than Reflect provides, allow a "backdoor" on external libraries, and allow descriptor to modify modifiers). This would bring far more power than the actual #field propose.

The public, private, protected should be allowed before static too.


To allow external classes or function to access to a protected or private member, we could use the friend keyword. Something similar to

class A {
  friend B
  private attr = 3;
}

This means than the class B can access the attribute attr of A even if it's private. We could then extends the Object.defineProperty :

interface Descriptor {
  ...
  friends: any[]
}

friends could be an array of friend functions or classes.


Finally for modifiers, a const keyword could be used to specify constant attributes, only allowed to be initialized into the constructor.

class A {
  private const attr = 3;
}

This will be a shortcut for writable=false

2) Multiple inheritance

Multiple inheritance is something that a lot of developers wants (there is a lot of subject or tutorial on the web) but can only be archived through mixins or factories. I would enjoy that you won't reply : "this is too complex because of diamond structures" or whatever because this is obviously FALSE (other languages like C++ archive it easily).

The following is just an idea how to bring multiple inheritance, it's not a concrete proposal.

First of all, because of the javascript current inheritance system with prototype, we can only inherit from one class. No more. A.prototype = Object.create(B.prototype); or in es6 class A extends B.

So we should introduce some king of new syntax.

1) We could use for example a new attribute prototypes which would be an array of mother classes, and prototype would point on the first element of this list ensuring retro-compatibility.

A.prototypes = [Object.create(B.prototype), Object.create(C.prototype)]; A extends B, C

2) instanceof should then search for all subclasses, so new A() instanceof C would return true.

3) The super keyword will need some adjustments: I propose this king of syntax super<B>.method : the super class here is B. To init a class:

constructor() {
  super<B>(...params);
  super<C>(...params);
}

Or we could use some C like : super::B. Using super.method will call the first super class (here B) for retro-compatibility.

Some other fancy stuff could be added too :

3) Abstract classes

Not the most important but really enjoyable, the abstract keyword could be use before the class keyword to define abstract classes. An abstract class may have abstract members and can't be initialized.

This still need more specifications.


So, the discussion is open. My purpose it to bring more in a big step to ecmascript instead of doing small steps and be stuck with retro-compatibility because of too various incremental specifications.

claytongulick commented 7 years ago

@littledan I certainly understand how it must be exhausting seeing similar discussion points come up repeatedly, and @bakkot has been a champ in here engaging with everyone and explaining the perspective and goals of the proposal.

Another way of looking at it, of course, is that the reason the same objections continue to be raised is because there is a large part of the community that's pretty unhappy with the proposal as it stands. I think it was @bakkot who said that after folks got used to the syntax, the percentage of folks who disliked it kind of evened out. Which tells me that there are still a ton of folks who still don't like the syntax, even after being exposed to it for a while. The only response to this flood of feedback has been something along the lines of "well, we weighed that and decided to move forward anyway". Over on a a related forum where this subject came up there were comments discussing how TC39 doesn't listen to the community, and I'm afraid that the responses here could be reinforcing that perception.

At this point, the proposal is at Stage 3, which means we hope the design and specification are basically done, and that the proposal is stable enough to be implemented with confidence. I'd hope that, in the future, this sort of feedback can be gathered before Stage 3. I'd appreciate any ideas to improve openness to community feedback and engagement.

Well, there's a couple things here.

One, is that this proposal, even given how contentious it's been, was sort of slipped in to the overall class properties proposal, in a 'unified' proposal and then rapidly pushed to Stage 2. This felt a bit like slipping a controversial piece of legislation into a spending bill (that's probably a terrible analogy, but you get the point). I know that I really wasn't aware of the proposal until I randomly checked on the progress of class fields (which I'm excited about) and saw the private syntax.

Two, as @bakkot mentioned:

Stage 3, the stage this proposal has just reached, is the stage designated for collecting feedback. We've already been doing so for years, and I expect we'll continue to do for some time.

If Stage 3 means de facto approval, and the voluminous public feedback isn't going to slow down the approval process, why even have a Stage 3? I understand the stated goals of the proposal, and I understand that the proposed syntax is likely the only/best way to technically accomplish the stated goals of the proposal. I think the point of consideration is whether there's widespread agreement on whether those goals are the most important thing to the community. Also, as a side note, it's not as if there was a dearth of feedback on this prior to Stage 3.

I know you and @bakkot have thought about this for a long time, and have put an enormous amount of effort into it, but I can't help but think that @pitaj is spot on about splitting this back into a separate proposal, and pushing forward with the rest of it.

Reducing the problem of access modifiers to the minimal case of privates, and pushing forward with a syntax that's limited to just that single case - seems like too narrow an approach, and I think that's what we're seeing a lot of the feedback boil down to.

I completely agree that the # syntax would work, and it might be the only technically feasible way to meet the goal of hard privates and strict encapsulation. But it certainly doesn't address or solve the problem of access modifiers in general, and this seems to be a more important issue to many people than hard privates. This is the thing that I, and others, are asking TC39 to take into account - it's more important to us to get access modification right for all of the cases than it is to solve the narrow problem of hard privates, especially when the syntax is in a form that could cause an inconsistent approach in the future - # for private but @decorator for protected, etc...? Can you imagine trying to teach someone new to the language this? And try to explain why the language is good despite this inconsistency? Anyone, on the surface, seeing that would say "that's just bad language design". How would you defend that point? It'd be tough, right?

littledan commented 7 years ago

@lifaon74 I agree with you that friends, protected and soft private are very useful. It would be great to work with you on a follow-on proposal for these things.

What I'm working on towards that right now is a version of the decorators proposal. It's actually a pretty different proposal from what Babel and TypeScript implement right now, and does not involve modifying Object.defineProperty. I'd be happy to have your help there, or in another proposal for friendship.

The way I'd explain the design to someone new is that #x defines something completely internal to the class, unavailable from the outside, and then some decorators will expose it to some other scopes. That's how the language works, and I hope it'd be a learnable mental model.

There's been clear community feedback that they would prefer the private syntax. However, it also seems that what TypeScript does wouldn't scale as a language feature for several reasons documented in this thread and elsewhere.

There's definitely a lot of use for private--people have been waiting for this feature for a while. It's not clear to me what more we will learn by waiting. I've been working on the decorators proposal largely in order to show that this all lines up well, and I haven't found any contradictions yet.

mbrowne commented 7 years ago

If our position is that "we need to decide now" whether to offer native support for hard vs. soft private (only one or the other) then it's inconsistent to say "we'll figure that out later" regarding other access modifiers like friend/protected. Not that anyone actually said the latter, but the plan for supporting other access modifiers either in userland or natively should be discussed more and have strong community support before the hard private feature is made official.

I think decorators could be a viable option for the other access levels - thanks @bakkot and @littledan for explaining how that could work. I largely agree with what @claytongulick said about the importance of solving the general problem of access modification and having a coherent language design. On the other hand, # has the potential to act as a security feature, so in that sense it's different from other access modifiers so having a different syntax for it might not be a bad thing.

But there's another thing that bothers me about the decorators solution: since it's in userland, different people would implement it in different ways and we'd end up with a situation similar to what happened with object creation prior to classes, where there were multiple competing and incompatible libraries. If decorators are the solution we want to promote, then I would say it might even make sense to provide default implementations for @friend and @protected...and maybe @private too although I'm not sure that's a good idea given that # already means (hard) private. These default implementations could still be overwritten, but at least there would be an official standard that the vast majority of people would use.

Thinking about what this might look like...

#myPrivate

@inspectable
#mySoftPrivate

//or (if possible, with some extension to the decorators proposal)
@private
mySoftPrivate

@friend
myFriend

@protected
myProtected

(I included @friend and @protected rather than @internal since it's unclear to me how one would implement internal/package level access in userland, but I've brought up internal enough already and we can hopefully discuss that more later.)

It might be best to leave out a default implementation for soft privates, since if you wanted to allow reflection you could just use @friend or @protected or implement your own soft private if you really wanted access to be more limited...if the language were to provide a default implementation, @inspectable would probably be the better option since it would be less confusing than having both # and @private.

I still don't really like the inconsistency, but with official default implementations for something between public and hard private, I think this would be an acceptable solution.

lifaon74 commented 7 years ago

@littledan I'have finished to read the decorators proposal but I see some problems for access modifiers :

Decorators are cool but they are monkey patch too if nothing is standard for access modifier...

Finally, I don't see any problems to extends Object.defineProperty, it will remains compatible with older js versions and could be polyfillable.

littledan commented 7 years ago

@lifaon74 If you want to file bugs against the decorators proposal, that could be the best way to proceed here.

no standard "private", "protected", etc... everybody can do whatever he wants and must re-implement for every projects

We could have a standard library of decorators; @wycats has advocated that. Designing such a library would be the "easy part". It could also be in a follow-on proposal, once the community shakes out what's really important.

do no cover reflection because there is no way to modify property after (or user implementation use Symbol, but tiers must be aware of this...)

I'm not sure what you're getting at.

far slower than native or direct access because of getter/setter involved

It's true that accessors come with performance cost. I was picturing that, when using things within the class where it's defined, you may refer to the original private member, and just use the accessor from the outside. This might mitigate the cost. JITs can also mitigate it, but it's harder to optimize from the baseline case.

no proper way to get the stack (function.caller doesn't work in module), so no proper "private"

Doing things with the callstack for privacy sounds like a fun toy, but I hope it's never really used. I don't think this point should be relevant in any way.

littledan commented 7 years ago

@mbrowne Would your concern be entirely alleviated by a decorator standard library, or are there lingering additional issues?

lifaon74 commented 7 years ago

@littledan

I already did a POC but without anything native (private, protected, etc...) it's pretty slow and not totally accurate but it works. Maybe you have a better idea how to implement such a soft private decorator, could you provide an example/POC ?

Seeing this limitations, the standard "private", "protected" "friend" decorators should have an "native" control aver classes (more than could achieve current decorators).

But for me, decorators are not the best solution for private/protected... just some kind of monkey patch...

littledan commented 7 years ago

A decorator modify the property when it is parsed/evaluated by the JIT compiler, after that there's no way to re-decorate a property (from an external function for example). How could we modify the property access after that (reflection) ?

Can you give an example of a use case here? Decorators being restricted in this way is a design goal, not an accidental limitation.

A "private" with friendship requires to know the stack to check the access of a property to answer the question : is the caller a "friend" ? Same for "protected".

Could you give me an example of a programming language with these semantics? My impression was that these things were enforced more by scoping rules.

A true native soft-private/protected could be far faster than decorators. For me this point is the most critical : JS is already slow, we should not get it slower when something native could be done...

Possibly. A detailed design proposal could help make more sense of this sort of claim. Previous designs for protected ran into some issues. I'm also a bit skeptical for designs which only support protected and not other kinds of friends (as we had previously).

lifaon74 commented 7 years ago

@littledan 1) To allow reflection (so modification of a private to a public for example => soft private) we need some way to modify the property. The goal here is to allow tier developers to fasten code execution by forcing access to private properties (see Blob example above). 2) Hum all of them no ? On compiled languages (C++, Java, etc...), when we try to access to a property, the compiler check if the caller is in the list of the friends, if no, a compilation error is triggered. I expect the same behavior for js, but the check will be at each get/set of a property, so the Function.caller is required here. 3) Yes, friend is a must have. "private" has an implicit own class as friend and "protected", the class and all the derived classes.

littledan commented 7 years ago

see Blob example above

The blob example seems like an example of something that's known to be protected/available to friends when the class is written. When do you need further dynamicity/reflection?

Hum all of them no ? On compiled languages (C++, Java, etc...), when we try to access to a property, the compiler check if the caller is in the list of the friends, if no, a compilation error is triggered. I expect the same behavior for js, but the check will be at each get/set of a property, so the Function.caller is required here.

Just because JavaScript is dynamic doesn't mean we want all new features to be equivalent to doing eval all over the place. Checking the stack at runtime is basically just as bad. I can't think of any programming languages that go that route. I imagine both implementers and language theorists would be opposed to this sort of strategy.

JavaScript has lexical scoping, and if you're in strict mode (e.g., in a class body), then there are some well-understood rules about how names are visible, captured by closures, etc. You can tell whether a lexically scoped variable is visible to a closure based on where it's defined in the source code, rather than what the stack looks like. It'd be nice if new things that we do with names have this sort of regularity as well, if possible.

lifaon74 commented 7 years ago

The blob example seems like an example of something that's known to be protected/available to friends when the class is written. When do you need further dynamicity/reflection?

What if I'm a tier developer and and want to access the Blob buffer property (private) ? I need a way to add my own functions/classes as friends.

For the second point, I don't see any other solution than checking the stack to know if the current function has access or not to a private property with friend... The stack is already present through Function.caller (not in module) and Error.stack, so it is known by the browser. This is that we want with access modifiers : allow some functions to access a property and limit/reject for others.

bakkot commented 7 years ago

@claytongulick

This is the thing that I, and others, are asking TC39 to take into account - it's more important to us to get access modification right for all of the cases than it is to solve the narrow problem of hard privates, especially when the syntax is in a form that could cause an inconsistent approach in the future - # for private but @decorator for protected, etc...? Can you imagine trying to teach someone new to the language this? And try to explain why the language is good despite this inconsistency? Anyone, on the surface, seeing that would say "that's just bad language design". How would you defend that point? It'd be tough, right?

So, something I haven't really brought up on this is - people coming from different languages have different lists of accessibility modifiers they expect to be present and different ideas about what they do:

And that's not even getting into more subtle distinctions like whether nested classes have access to private fields of their containing class, whether containing classes have access to private fields of their nested classes, the extent to which any of these are enforced vs just suggestions, etc.

This says to me that no one has gotten accessibility modifiers right for all of the cases, at least if "right" means "satisfying the expectations of developers coming from all other languages". And we could imagine other modifiers not implemented in any of the mainstream languages, like "accessible only to code which possesses this token, which can be passed around".

If there is any way at all we can avoid getting into that mess, I would like to do so. It seems far better to me to provide the minimum necessary and then allow user code to define other modifiers to fit their needs, including modifiers which have yet to be invented, and maybe someday provide more after we've gotten some experience in how this bare minimum plays out in JavaScript. I'll stand behind that as good language design any day.

mbrowne commented 7 years ago

@mbrowne Would your concern be entirely alleviated by a decorator standard library, or are there lingering additional issues?

I'm still thinking about it...I think a standard library would work fine for friend/protected. The part I'm unsure about is, would there still be a common use case for soft private if we already had hard private, friend, and protected? If you need to expose what would ordinarily be a private property because you want to do reflection on it, or expect others will need to do so, then you've already broken real encapsulation anyway so protected would be sufficient if you can accomplish the necessary reflection via subclassing. friend is trickier because it would only be helpful in your own library or app (as far as I understand it), not for reflection of a 3rd party library. internal could be a viable solution to the last point, but I think that would require changes to how exporting modules works, requiring a new proposal.

ljharb commented 7 years ago

@lifaon74 what is a "tier developer"? Regardless, if something is private, the point is that there is no way you can access it short of modifying the code that makes it private. If the Blob class wanted its buffer property to be accessible, it wouldn't have made it private. The author of the code should be in charge, not the consumer of it.

claytongulick commented 7 years ago

@littledan @bakkot and @lifaon74 I'm really confused about the message re: using decorators - didn't we have a pretty lengthy discussion about how that can't work? We'd need to revamp Function.caller or add a 'this' context to Proxy traps or something to make it work - that issue was closed because it wasn't considered feasible. How is it that I keep seeing decorators put forth as a solution to access modification? Don't all the same issues still apply?

It's sounding increasingly to me like the pressure to make a decision now on privates is really one that's going to cost the language in the long term, since many of the same constraints and considerations apply to protected and others.

@bakkot I agree completely with your point about the wide and varied way the other languages approach access modification, and how none of them really nail it, strengths and weaknesses all around. I think that's why I liked having an extensible solution in userspace with some default implementations in the decorator core library.

bakkot commented 7 years ago

@claytongulick, decorators are insufficient to achieve the level of privacy offered by this proposal. However, given that level of privacy, you can build other types of accessibility modifiers on top of it, which @littledan has given a few examples of (though I expect more to be invented once decorators stabilize).

claytongulick commented 7 years ago

@bakkot ok, that makes more sense to me, thanks! @littledan 's point about protected being leakable anyway is a strong one. Other than the obvious performance impact (which as I've mentioned before, isn't a huge consideration for me, because variable caching) I can see how this would work having decorators for access modification in userspace. I do agree with @lifaon74 that in order to make this truly robust, we'd need some calling context, via a proxy trap or Funtion.caller or similar.

lifaon74 commented 7 years ago

@ljharb

what is a "tier developer"? Regardless, if something is private, the point is that there is no way you can access it short of modifying the code that makes it private. If the Blob class wanted its buffer property to be accessible, it wouldn't have made it private. The author of the code should be in charge, not the consumer of it.

A tier developer for me is someone using an external lib (not the author of the code).

To clarify things, this is my vision of the perfect private :

So in 99% of the time you just will not be aware of the presence of the private, and will not care of the possibility to force private access. But if you see in the source code that you can improve perfs by 1000 just by accessing a private properties, you should be allowed to force the access and add your own friend list.

@bakkot Accoding to the TIOBE index the most popular languages are Java and the C and its derivatives. I guess many of us base our expectations or "private", "protected", "friend" on them. Java and C have the same definitions of "private" and "protected" and allow reflection. For me C improve things with the "friend" keyword which allow less abuse of the reflection.

@claytongulick, @bakkot Yes, that's why I pointed the limits of decorators and why it doesn't really satisfy me.

bakkot commented 7 years ago

@lifaon74, a few points:

C and its derivatives

C doesn't have classes, or anything like private. C# has very different access modifiers from C++, as gone into above, even though both are C derivatives.

Java and C have the same definitions of "private" and "protected"

Assuming you mean Java and C++: No, they don't.

Java's private allows outer classes to access private fields of of inner ones; C++'s does not. Java's protected fields are also available to other non-child classes in the same package; C++'s are not.

and allow reflection

Not at all in the same way. Java has language-level support for reflection on private fields - but as I say above, they've found that unconditionally allowing reflection creates problems and are trying to back off on that. C++, meanwhile, has no language-level support for reflection; it's just that you have direct access to memory and so can sometimes mess with whatever you want.


Separately... "there are other popular languages which do things this way" isn't that convincing of an argument. Increasingly JavaScript programmers are not coming from Java or C++, and so will not share your expectations. Moreover, what is appropriate for one language might not be what's appropriate for another - and anyway other languages have sometimes made poor design choices which we should not blindly repeat.

comex commented 7 years ago

C++, meanwhile, has no language-level support for reflection; it's just that you have direct access to memory and so can sometimes mess with whatever you want.

C++ has #define private public :wink:

mbrowne commented 7 years ago

Responding to an earlier comment @bakkot (also in response to @jeffmo):

I'm not aware of any language that has both class and a notion of private fields in a class where the accessibility of those fields is per-instance rather than per-class. I listed a few examples here, though that list could be substantially longer.

Just because other languages do it that way doesn't mean they got it right. Javascript's OO model is actually object-oriented, whereas most so-called OO languages are class-oriented; it's a big difference. Non-prototype instance properties are truly on the instance, not the class/prototype, so in theory one object shouldn't know anything about the other object's private properties (even if they're instances of the same class), similarly to how encapsulation would work using a closure for this:

function User(ssn) {
  const socialSecurityNum = ssn;

  this.isSamePerson = (user) => {
    if (!user.socialSecurityNum) {
      throw Error('socialSecurityNum property not found');
    }
    return user.socialSecurityNum === socialSecurityNum;
  };
}

const user1 = new User(123);
const user2 = new User(456);

//throws the error 'socialSecurityNum property not found' since it's private
user1.isSamePerson(user2);

I remain unconvinced that the binary methods @jeffmo brought up are an important enough use case that supporting them natively should be a hard requirement of this proposal. Additionally, I would guess that the implementation might be simpler if enforcing it at the object level rather than the class level, though if the reverse is true then I would revise my opinion to say maybe it's not worth it.

But I'm not one to be too strictly theoretical; I like to be pragmatic too. The class author is after all in control of everything that happens in its methods, and I don't see any practical downsides to allowing binary methods to access private properties (of a different instance of the same class). If others view this as an essential feature, then I'll be quiet about this.

On the other hand, if we just view this as a "nice-to-have" feature that could alternatively be implemented in userland, then I would certainly hope that the decision about the syntax wouldn't be predicated on it (even though I personally like the # syntax).

ljharb commented 7 years ago

It's not just nice to have; being able to expose an "isArray"-like method that brand checks is critical.

I'm fine with instance-private also, but being able to hide implementation details of an instance from other code the class author wrote doesn't seem too convincing a use case; whereas class-private allows both brand-checking and hiding implementation details from "not the class author", and both are what my use cases require (and can't be done in user land easily/performantly/elegantly otherwise).

mbrowne commented 7 years ago

BTW I can think of a couple similar ways to accomplish the same goal...

  1. Introduce a final class feature for classes that shouldn't be extended, and if you need this feature then mark your class as final (you can use composition instead of inheritance if you need to extend its functionality). This would work in conjunction with protected -- protected meaning accessible to the class and any of its subclasses, except in this case there would be no subclasses so it would be encapsulated at the class boundary.

  2. Introduce friend objects in addition to friend classes, and make it possible to specify that all instances of a class should be friends with all other instances of that same class.

Probably neither of these are good solutions #1 relies on inheritance-based visibility, which I don't like anyway and using it in this way is kind of a hack, and in #2 the idea of objects of the same class being friends is weird). But it shows that class-scoped privates are not the only way to solve this, and maybe there's another alternative I didn't think of. (I don't feel all that strongly on this matter, but since it seems to be quite consequential I figured I'd be thorough.)

ljharb commented 7 years ago

@mbrowne I would need to be able to brand check my class instances but also allow them to be extended/mutated.

mbrowne commented 7 years ago

@ljharb

@mbrowne I would need to be able to brand check my class instances but also allow them to be extended/mutated.

Subclassing isn't the only way of extending a class's behavior, but I get why the alternative of composition/forwarding would be undesirable here. Assuming others also view binary methods as an essential feature needing native support, we can set aside this discussion.

mbrowne commented 7 years ago

@littledan

Just because JavaScript is dynamic doesn't mean we want all new features to be equivalent to doing eval all over the place. Checking the stack at runtime is basically just as bad. I can't think of any programming languages that go that route. I imagine both implementers and language theorists would be opposed to this sort of strategy.

I agree that most violations of access modifiers are detectable at compile time, but there are exceptions, for example:

function demo(propName) {
    //if propName is private, this should throw an error
    obj[propName]
}

I'm sure you've already thought about such cases, and it looks like the private properties proposal already handles this at run-time by checking the scope of the caller that tried to access the property. I assume it also would correctly handle this situation:

class X {
    #foo = 1;

    demo() {
        const fn = () => {
            this.#foo;
        };
        fn();
    }
}

How could the same thing be accomplished with decorators for protected/friend/internal?

bakkot commented 7 years ago

@mbrowne

You've misread the proposal a bit, I think:

function demo(propName) {
    //if propName is private, this should throw an error
    obj[propName]
}

This is always a public field access, even if propName starts with #. Private field access looks like .#. So no, it doesn't throw an error; it just accesses a public field.

it looks like the private properties proposal already handles this at run-time by checking the scope of the caller that tried to access the property.

No, not quite. There's two checks: first, a static check which ensures that the private field is lexically in scope when referred to; second, a run-time check on the receiver which ensures the receiver has the private field.

I assume it also would correctly handle this situation:

If by "correctly" you mean that calling demo on an instance of X would invoke fn and fn would return 1, then yes. Any code in the body of the class has visibility of the private name and so is allowed to refer to it, and since this in fn will be an instance of X and so have the private field, the private field access will succeed.

How could the same thing be accomplished with decorators for protected/friend/internal?

I'm assuming that most use cases for decorators as accessibility modifiers would be built on top of private fields - it's relatively easy to go from maximally private to less private, whereas it's much harder to go from maximally public to less public. So they'd all get the above behaviors automatically, unless they specifically circumvented them.

mbrowne commented 7 years ago

@bakkot Thank you for correcting my misunderstanding of the proposal. I see how one could use a combination of decorators and private fields to somewhat emulate protected (protected meaning allowing access to subclasses) by decorating both the parent class and the subclasses (although I don't know if it would work for super). But I don't see how this would work for friend classes. The JS engine can check the lexical scope of the caller; but as you pointed out, this happens at "compile" time (or perhaps it would be more accurate to say, before any of the code in the given declaration is actually run) and userland code cannot do this. So for friend classes, I don't see how private fields would make it any more possible to implement with decorators.

However, I'm all for the ability to check lexical scope of the caller in userland, if that's practical somehow. I figure there must be some performance or security downsides and technical challenges with that or it would be allowed already. But that ability would give users the most flexibility to implement custom access control, and would also make it possible to do some interesting things that are currently impossible in most languages including JS, such as properly implementing DCI. (The closest I was able to come to real DCI in native JS was requiring the use of a context identifier with dot notation so that I could determine what scope a method was being called from - if interested, see https://jsfiddle.net/mbrowne/uvL1uk5a/. The context identifier in this example is transfer.)

mbrowne commented 7 years ago

P.S. Although it could be useful in the case of dynamic property access, I'm not suggesting that the ability to check lexical scope should necessarily happen at run-time. There could perhaps be a feature to give an instruction to the JS "compiler" to check this statically, which of course would be more performant.

allenwb commented 7 years ago

@mbrowne

I see how one could use a combination of decorators and private fields to somewhat emulate protected (protected meaning allowing access to subclasses) by decorating both the parent class and the subclasses (although I don't know if it would work for super). But I don't see how this would work for friend classes

For an example on how a slight extension to the current private field proposal would enable "friend access" see https://github.com/tc39/proposal-private-fields/issues/93#issuecomment-309041543 for context you probably need to start reading at https://github.com/tc39/proposal-private-fields/issues/93#issuecomment-308871218

bakkot commented 7 years ago

@mbrowne Yeah, I don't think there's a way to implement friend classes exactly as they work in C++, i.e., such that the friend class can do property access directly. (You actually could make it work if private accessors didn't branch check their receiver... but probably we want them to.)

However, you could do something like

class Friend {
  @friendReader // defines some method on Friend, e.g. `static setReader(f) { this.#fooReader = f; }`
  static #fooReader;
  static peek(obj) {
    console.log(this.#fooReader(obj));
  }
}

class Private {
  @withReader(Friend) // calls `Friend.setReader(obj => obj.#foo)`
  #foo = 0;
}

let p = new Private();
Friend.peek(p); // prints 0

which I think pretty much accomplishes what friend classes are supposed to accomplish, except on a more granular level.

In practice @friendReader and @withReader would coordinate using some Symbol they'd both closed over, rather than a string-named property, to avoid conflicts. Decorators adding string-named properties is probably bad practice.

If you didn't want that granularity, you could also do something like

class Friend {
  @friendMultiReader
  static #reader;
  static peek(obj) {
    console.log(this.#reader.foo(obj) + this.#reader.bar(obj));
  }
}

@friend(Friend)
class Private {
  #foo = 1;
  #bar = 2;
}

let p = new Private();
Friend.peek(p); // prints 3

You see how this approach is a lot more flexible than us defining friend directly, yes? Even if it is a little less convenient to use - and even that inconvenience might get better someday.


I don't want to overstate my case here. I'm not saying that private fields + decorators are definitely the only way we'll ever have any other accessibility modifiers. Decorators are only stage 2, after all, and there's no guarantee they'll make it into the language.

Just, I feel confident that we need private state, and that this proposal is the best way to implement private state - that even if we did want to provide other types of accessibility, we'd still want to do private state pretty much as it is in this proposal. And it seems like private state + decorators could be used to provide very fine-grained accessibility for people who want other kinds of accessibility than just hard private. So I would like to more forward with this proposal, without any further types of accessibility built in, and let the community use it for a while to see what problems and solutions arise. I think that's better than trying to come up with the entire story from the ground up, especially, again, given how diverse other languages are wrt this particular problem.


However, I'm all for the ability to check lexical scope of the caller in userland, if that's practical somehow.

I think this is probably off the table. There's a reason we killed arguments.caller.

lifaon74 commented 7 years ago

@bakkot thanks for these great examples. This should be a part of tutorial/doc example before the official release of #. But even if a solution exists,I still consider it very complex to implement and verbose, versus private/protected and friend => something really difficult for a beginner, when a single keyword could save a lot of work... Currently with typescript, such a behavior is achievable but not performant and pretty complex.

mbrowne commented 7 years ago

@allenwb @bakkot Thanks for the links and explanation.

My main concern was that we had thought far enough ahead about how to handle other access modifiers in the future that we know it would basically work even if there are details left to be sorted out. I see that you have thought through it. So I agree it's a good plan to go forward with the current proposal so that JS has some sort of privacy for properties sooner rather than later, hopefully soon followed by private methods and of course decorators. The friend syntax you demonstrated is a little awkward so I wouldn't standardize that, but I would certainly vote for @protected making it into a standard decorators library if that's the most practical first step to offer something between private and public...unless it begins to look more likely that there would be native support for that, of course.

However, I'm all for the ability to check lexical scope of the caller in userland, if that's practical somehow.

I think this is probably off the table. There's a reason we killed arguments.caller.

I suspected that might be problematic. Well, arguments.caller or Function.caller wouldn't be sufficient anyhow, because you'd need to know not only the caller but also the scope in which it was defined.

Here's another idea to consider for the future...what if we had a special decorator (or perhaps some other syntax) to explicitly specify a statically-evaluated scope restriction for the caller? For example:

class Private {
  @__allowedCallerScopes(Friend)
  #foo = 1;
}

Obviously this would be a special decorator implemented natively, hence the __, and if the decorator syntax were confusing then some other syntax could be used. In any case, it would be nice if __allowedCallerScopes were something that could be specified within a decorator, or if custom decorators could extend from it, allowing you to create nice abstractions over it like:

  @friend(Friend)
  #foo = 1;

I don't know how practical this is from an implementation perspective, but it certainly would afford a lot of flexibility and creative and elegant solutions to access control.

bakkot commented 7 years ago

@mbrowne

The friend syntax you demonstrated is a little awkward so I wouldn't standardize that

Yeah, agreed. Also, the whole concept of friend is weird, especially in JavaScript - it makes sense that a class should be the main unit of privacy with regards to its own fields, but when granting that access to something else, why should that other thing necessarily be a class?

I think I personally would instead do something like

let fooAccessor = {};

class Private {
  @withAccessor(fooAccessor) // sets `fooReader.get = o => o.#foo`, `fooReader.set = (o, v) => o.#foo = v`
  #foo = 0;
}

fooAccessor.get(new Private()); // 0

and then pass around fooAccessor to wherever direct access is needed.

Anyway, like I say, this is something I wouldn't want to standardize without the community first having a chance to use hard private fields in JavaScript and figure out what things they need.

I would certainly vote for @protected making it into a standard decorators library if that's the most practical first step to offer something between private and public

I think the right thing is an @inheritable / @inherited decorator pair, rather than @protected, but yes, this would be a good thing to provide and/or teach.

mbrowne commented 7 years ago

Also, the whole concept of friend is weird, especially in JavaScript - it makes sense that a class should be the main unit of privacy with regards to its own fields, but when granting that access to something else, why should that other thing necessarily be a class?

Agreed. You might have noticed that in my example, I created a friend relationship at the property level rather than the class level - not sure how useful that would be, just wanted to show the possibilities for custom access.

and then pass around fooAccessor to wherever direct access is needed.

Would there be any issues with all dependent modules needing fooAccessor to be loaded before the Private class was loaded? Unless you meant exporting fooAccessor, but then it would be very easy to get around the access restriction, so I assume you meant the owner (in this case Foo...or rather the module containing Foo) would be the one passing the accessor.

I think the right thing is an @inheritable / @inherited decorator pair, rather than @protected, but yes, this would be a good thing to provide and/or teach.

Yes, that's better.

bakkot commented 7 years ago

Would there be any issues with all dependent modules needing fooAccessor to be loaded before the Private class was loaded?

Yes. But I think that's inevitable; the code ultimately needing access to #foo has to be available at some point when someone who has access to #foo is around to grant it. Otherwise you could go from "no one has access to #foo" to "someone has access to #foo", which requires breaking encapsulation.

__allowedCallerScopes(Friend) ... I don't know how practical this is from an implementation perspective

Unfortunately, pretty impractical, since identifiers like Friend can't generally be resolved statically.

mbrowne commented 7 years ago

__allowedCallerScopes(Friend) ... I don't know how practical this is from an implementation perspective

Unfortunately, pretty impractical, since identifiers like Friend can't generally be resolved statically.

What if it were at the module level rather than the class or function level, e.g.:

@__allowedCallerScopes('./Friend.js')

Of course this assumes the use of native ES6 modules.

bakkot commented 7 years ago

What if it were at the module level rather than the class or function level, e.g.:

I'm not sure how feasible that would be technically, but I don't think we are likely to introduce that kind of interaction with module resolution and loading in the near future; those pieces are already fairly complicated and it would not be worth burdening them further just to solve this problem.

lifaon74 commented 7 years ago

@mbrowne I like the concept of friend module. Something interesting.

Maybe guys could speak about your expectations when developing a package. My personal abstract vision:

The definition is concise, please don't play on the words...

My vision is centered about 2 key points :

Such a behavior is possible by having multiple modules exporting all the classes and methods and hidden "private" (enumerable: false) classes properties (by exporting everything we allow any package to import our modules) and by creating an index.js exporting only the "proper" implementation. The only problem, is that no check is done at any time (is it a friend classe/function, is a a private method, what is the call stack ?) and we have no way to warn the user dev on a mistake. Moreover, the "hidden" properties strongly lacks of access control. What I mean is : we can check the type of a variable to warn a developer about a type mistake but they is no way to know if the access to a property is currently legitimate, so no way to warn about a bad access.

From here 2 solutions are possible :

ljharb commented 7 years ago

@lifaon74 I have lots of uses for "friend" in the sense that I have things that I want to be hard-private, but I need to share them across multiple files. The problem is that there's no way in JS besides scope privacy/closures to do that - and anything that doesn't offer guarantees that can prevent "unauthorized" access isn't any more helpful than "just using a Symbol".

I'd be very interested to read a separate proposal that made this feasible; but it just doesn't seem worth it if it can't provide hard runtime guarantees.

mbrowne commented 7 years ago

@lifaon74

we let the developer handle by himself the access control. For this we need to provide him a proper call stack (which function call this property ?, from which module ? and with which scope ?). With this we could imagine really precise control from the developer. I truly like this solution.

As we discussed above, this doesn't need to happen at run-time but can happen just once when the code is first interpreted. That way you wouldn't need to access the call stack...I seem to recall that accessing the call stack at run-time significantly slows down your code (someone please correct me if I'm wrong), and it obviously would be less efficient to check the call stack every time a restricted property is accessed than to do it just once ahead of time. Run-time checking would also be awkward unless you had some helper methods like isInScopeOf(MyClass) - certainly it would not be ideal to just have something like caller.parentScope.parentScope...., since you'd have to keep checking all the way to the top-level scope (remember that nested classes are possible).

This is not to say I'm totally against the idea of run-time checks; they could be useful for things like dynamic property names as I mentioned above, but this definitely should not be the primary means of restricting access. There are a lot more performance and security considerations than I've mentioned here, so I think for now it would be best to focus on access restrictions at interpretation time - so that means either a native solution for protected/friend, or some sort of annotation to explicitly check scopes at the module level that would be evaluated at interpretation time, not run-time.

This is how I see things in terms of priority:

  1. Private fields
  2. Private methods
  3. Decorators

Decorators can be used at least to implement reflection on private fields, even if we go with a native solution for internal/protected/friend. For a proper implementation of friend or internal, I think decorators are insufficient unless users also have some way to check the scope of the caller (definitely at interpretation time, and maybe at run-time too, but IMO module-level restrictions would be sufficient). We've discussed a lot of pros and cons here, so I'm not sure of the best path forward for internal/protected/friend, but fortunately we don't need to figure everything out 100% right now, and it seems we have multiple options still on the table for that.

lifaon74 commented 7 years ago

@mbrowne Because of the really dynamic behavior of js we can't properly do this check at compile time. What about eval ? What about function.apply, etc... Too many cases where the compiler can't anticipate what the user will do. Currently, yes the call stack is slow to access, but only because no proper and standard functions exists. I assume a native solution would be faster. And yes, checking every time will be slower than once, just like type checking, BUT it would be a choice of the developer (it's a compromise : strong check vs fast execution, we let the choice).

mbrowne commented 7 years ago

@lifaon74 I was just going to say, I realized that my comment about the access check being done "just once" was misleading because of cases like this:

function demo(obj) {
  ...
  obj.protectedProp;
  ...
}

I don't know enough about JS internals to say exactly how a proper implementation should handle this, but I'm pretty sure that you still would want to do the access check prior to when the line of code accessing the property (obj.protectedProp) is actually executed. You're right, in some cases it would have to be a dynamic check, but I'm not convinced those cases are particularly important, and IMO even if we do need to support access restrictions for such cases it could wait till a much later proposal. I don't think it's an unreasonable limitation to say that if you want to access restricted properties, you have to use dot notation (and otherwise the property would act the same way as a hard private) -- although I'd prefer to ultimately have the option to do either a static or dynamic check, as you suggested.

ljharb commented 7 years ago

@mbrowne those cases are absolutely critical; without them I can't make an equals method that uses private data to compare two instances, nor do a static brand check.

mbrowne commented 7 years ago

@ljharb I wasn't proposing any changes to hard private fields. I was only talking about access levels between hard private and public. If those can work just as well as private fields with regard to truly enforcing the access level, great. I'm a bit confused...is your comment in response to my latest comment or an earlier one?

ljharb commented 7 years ago

@mbrowne

You're right, in some cases it would have to be a dynamic check, but I'm not convinced those cases are particularly important

In the case of friend/protected, I'm saying those cases are indeed particularly important.

mbrowne commented 7 years ago

@ljharb Do you have an objection to this proposal moving forward with only hard private properties, and hopefully adding either native support or a more robust native/decorators hybrid for friend/protected in the future? That sounded like a reasonable plan to me.

mbrowne commented 7 years ago

BTW, I haven't heard the term "brand check" before. Since you mentioned isArray, I'm guessing you want this for the same reason that instanceof is insufficient for arrays - so it works across frames.

loganfsmyth commented 7 years ago

It's useful to check "this object was created with this constructor", because anything can create an object with a given prototype chain, but that doesn't mean that the object is actually an Array object or whichever. The array case is useful across realms, but it applies within a realm too. For instance if you do

var map = Object.create(Map.prototype);
map.set("foo", "bar");

that will throw because the object returned isn't a real Map object, it is just an object that has the Map prototype in its chain.

ljharb commented 7 years ago

@mbrowne yes, precisely - instanceof is useless for me because it's not cross-realm, and because it can be easily broken by mutating the [[Prototype]].

And no, I want this proposal to move forward ASAP with only hard private fields (and public fields, ofc), and I want anything beyond that to be a separate proposal.