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.

bakkot commented 7 years ago

The # for private sound wrong for me and the discussion didn't convince me

It's convinced the committee. Have you seen the FAQ? Do you think there's something it doesn't address?

Multiple inheritance / abstract classes

This isn't the right place to discuss those topics. You might be interested in this proposal.

That said, we are extremely unlikely to introduce significant breaking changes, especially to the inheritance model.

lifaon74 commented 7 years ago

Yes, everything read. For me the private proposal is currently more "theorical" but doesn't really solve deeply daily developers problems :

export class B { friend A private attr = 'B'; }


- allow to create and modify a private field with `Object.defineProperty` or kind of => a external library could use a private field and for whatever reason we may be required to access this property (because of inheritance, performance by accessing direct member instead of getter, etc....). This should of course be used as a last resort, but in practical this kind of situation may appears. Reflect or Proxies allow us crazy things, we should not be blocked because of a totally unreachable private member. I understand that encapsulation is important, but in some case we really need to break it. Moreover, modifiers are first intended to provide "informations" to the developer (I know that this class has a private `a` because I should not access it, but is used by the class itself, same for _protected_).

```js
class A {
  #data;

  setData(data) {
    // some super slow type checking, data conversion, etc...
    // ex:
    for(const key in data) { /* whatever */ } // imagining it takes 1s
    this.#data = data;
  }
}

Knowing that #data exists and having the perfect datawe could directly set data in #data.

When accessing a property, the browser knows the context and could easily determine if the property could be accessed or not. If others languages can do this (and fast), ecmascript can do this too.

For me the proposal is not wrong, but don't go enough far on the expectations a developer could have for modifiers. ES6 introduced big features with new syntax, so we should not be afraid. If it solve a problem (and it will) the community will accept it fast (and polyfill/transpillers exists for old browsers).

littledan commented 7 years ago

Friend classes are really important. My current thought is that this capability would be provided by decorators. There's a hard problem where it would be impossible for two classes to declare themselves to be mutually friends if declaring friends is set up by including a declaration which references the other class, since classes are always defined one after the other. Decorators can get around this by writing the friend information to a third location, wherever they want.

lifaon74 commented 7 years ago

I wrote a fast POC to demonstrate the feasibility using decorators => I use the Error stack trace to determine if called from a friend class/function. The problem by using decorators instead of native : it's far more slower (~1500 times slower, which result in "only" 50k calls/s). Moreover, the current private proposal doesn't allow to modify on the fly the property access, so they are not editable by decorators.

There's a hard problem where it would be impossible for two classes to declare themselves to be mutually friends if declaring friends is set up by including a declaration which references the other class, since classes are always defined one after the other.

Well in fact with the new Object.defineProperty I proposed upper, inside of constructor we could create on the fly private properties and friendship (after all classes are defined).

littledan commented 7 years ago

I don't think using the error stack is a good idea. Not only is it slow, but it can also be spoofed in strict mode code (e.g., class bodies) because you don't have access to function identity in the V8 stack trace API.

It's not clear when "after all classes are defined" is, in a world with eval, dynamic module import, and dynamic script tag insertion. Code is always coming into a JavaScript system.

lifaon74 commented 7 years ago

Well, it's a POC, so far far from being perfect.

It's not clear when "after all classes are defined" is, in a world with eval, dynamic module import, and dynamic script tag insertion. Code is always coming into a JavaScript system.

What I mean is : it's the responsibility of the developer to know if the class exists (has been parsed by the browser). If he knows it's fine (ex: in a constructor of a class, after a window.onload for example, class B exists), he could create the private property and the friendship on the fly with Object.defineProperty.

claytongulick commented 7 years ago

@lifaon74 you might want to take a look at the discussion here for an expansion on the idea of using decorators as access modifiers.

Adding modifiers to the descriptor is an interesting idea as well, and could then in turn be trivially managed syntactically with decorators.

lifaon74 commented 7 years ago

@claytongulick Well at the moment, the big drawback I see is the fact that we can only access the stack with Error().stack because function.caller is not available into es6 modules :'(. It's far from being perfect. So for a proper polyfill of private/protected decorators the function.caller should be allowed inside modules, properly defined in the specs (because it seems not being a standard), or some kind of stack() function should exsists.

claytongulick commented 7 years ago

@lifaon74 I think your proposal for having modifiers on the descriptor makes way more sense than the dynamic run time check via caller or an enhanced Proxy (though I do think adding caller info to Proxy is a worthy endeavor on it's own). It's sort of the best of both worlds there - it's straight forward for library space to manipulate, expandable, and achieves most of the goals that @littledan and @bakkot laid out.

One of the problems I'm having is that the justification for # in the FAQ doesn't seem consistent to me. Some examples:

Having a private field named x must not prevent there from being a public field named x, so accessing a private field can't just be a normal lookup.

I really don't understand this. Having a private and public member with the same name isn't allowed in other languages, why are we trying to do it? Java and C++ certainly don't allow this.

If I have

class Foo {
    @private //modify the descriptor to make 'a' private
    a=0;
}
(new Foo()).a = 5; //why not throw here?

in JavaScript this would silently create or access a public field, rather than throwing an error

That's how it works now, but the whole point of adding a concept like private implies that this behavior would change. Given that the convention in a lot of OOPy languages is to prefix privates with an underscore anyway, I don't see that causing a problem with naming conflicts. If someone felt really strongly about adding a public to an existing class that has the same name as a private, there's always Object.defineProperty that can be used to stick a getting/setter on it.

If private fields conflicted with public fields, it would break encapsulation; see below.

This is another point that I don't understand well.

class Base {
  @private  //set access modifer in the descriptor to 'private'
  x = 0;
}

class Derived extends Base {
  x = 0;

  foo() {
    x = 3; //spiffy
    super.x = 3; //error - the same way trying to do this to a writable: false prop would
  }
}
(new Base()).x = 3; //error - throw
(new Derived()).x = 3; //fine and dandy

That seems like natural and straight forward behavior, and what I think most folks would expect from classes coming from other languages.

If we can't figure out a graceful syntax for privates, I think it's better to wait to add them rather than pulling the trigger on #. We can't put that toothpaste back in the tube once it's done.

bakkot commented 7 years ago

@claytongulick, the reason a private and public field of the same name must be allowed, and the reason that (new Foo()).a = 5; must not throw just because Foo has some private field, is that it breaks encapsulation.

This is covered in the FAQ, but to recap: Encapsulation is a core goal of the proposal because other people should not need to know about implementation details of your class, like private fields. (Indeed they should not be able to know about them, without inspecting your source, since otherwise those details are part of your public API and will be depended upon.)

For example, if I am a library author providing class Foo, I should be able to introduce to Foo a new private field a without breaking anyone who is extending Foo and adding their own a public field, including people who are just manually adding an a property to instances of Foo. For this reason languages like Java do allow classes to have both a public and private field of the same name, as pointed out in the FAQ.

Your example cannot work as described, because x is a field on the instance. If public and private fields are both just properties, with different descriptors like "writable", then there can't be two of them on the same instance. So, for example:

class Base {
  @private
  x = 0;

  static m(obj) {
    return obj.x;
  }
}

class Derived extends Base {
  x = 1;
}

Base.m(new Derived()); // does this return 0 or 1? How could it know?
claytongulick commented 7 years ago

@bakkot that's a great response and really helps me understand your point, the FAQ just sort of says "encapsulation" but doesn't go into much detail (there are several ways to achieve this without #) - this might be great info to add to the FAQ to help folks like me understand.

I'm no Java guru, but when I do this:

public class HelloWorld
{

  public static void main(String[] args)
  {
    OtherClass myObject = new OtherClass("Hello World!");
    System.out.print(myObject);
  }
}

public class OtherClass
{
  private String message;
  public String message;

  public OtherClass(String input)
  {
    message = input;
  }
  public String toString()
  {
    return message;
  }
}

I get:

/tmp/java_IZkI06/OtherClass.java:4: error: variable message is already defined in class OtherClass public String message;

Am I being dense here? Agreed that this would work on a subclass defining a public message, but that's a case that can be handled, I think (see below). Also, again, I'm no expert but IIRC C++ is particularly pissy about duplicate names because of the way mangling works and addressing.

Your example really demonstrates a great point about how that behavior would be currently undefined, but at the risk of gross oversimplification, couldn't we define priority rules to solve that? I.e., check if there's a public property first, if not, check for a protected, if not check for a private, if not, throw...

bakkot commented 7 years ago

Sorry, when I say Java "allow classes to have both a public and private field of the same name", what I really mean is that it allows instances to have a public and private field of the same name, by being instances of both a superclass which a private field and a subclass which has a public field of that name. That's the case the FAQ intends to get at, though in JavaScript it's a little more complicated because code external to a class can and often does add properties to instances of a class without actually subclassing said class.

couldn't we define priority rules to solve that?

We could, in theory, though I don't think it would actually solve the problem: if a class has a private field, and writes methods operating on that field, those methods shouldn't break just because a consumer of that class is adding a public field of the same name to instances of the class.

But even if it would work, we would strongly prefer to avoid complicating property access semantics.

claytongulick commented 7 years ago

@bakkot thanks again, I see your points and I think they are strong. I guess it comes down to whether we're willing to trade a bit of complexity with property access semantics for a cleaner syntax and avoiding a magic character like #. I think you can guess which side I fall on 😄

I'm not sure I have much more from a technical standpoint to contribute, I'm curious how others in the community feel. I do deeply appreciate the time you've spent discussing this with me!!

lifaon74 commented 7 years ago

@claytongulick I fact, the "private" field with # are "class scoped variable" it means the property can only be accessed from the context of the class (it's some king of the exact same behavior than WeakMap with classes). What I reproche is : that it's a different meaning of private keyword from other languages (maybe the name should be changed as "class scoped properties" to avoid confusion for other developers), the missing of "friend" because it's class scoped only and can' be modified afterward, the missing of "protected" which is really important, this missing of modifications in case a developer wrote a "bad" classe.

@bakkot The fact what a programmer should be allowed to have a private and a public property with the same name is for me irrelevant => with a Object.defineProperty private modifier, when the dev will run its script it will immediately see an error if he use a private name (like: "You're trying to overload a private property) and could simply change the name OR he could edit the property itself by renaming it for example (what the current spec disallow). Moreover, for me a developer should know the class it extends and its private fields (from the doc, a @Type/..., etc...).

bakkot commented 7 years ago

@lifaon74: As the FAQ says,

[Library authors] do not generally consider themselves free to break their user's pages and applications just because those users were depending upon some part of the library's interface which the library author did not intend them to depend upon. As a consequence, they would like to have hard private state to be able to hide implementation details in a more complete way.

Keep in mind too that it's not necessarily the case that the person whose code breaks is the person who has to deal with that problem: suppose C depends on B which depends on A. If A adds a new private field in a patch update and in doing so breaks B, then C will update A and find that their project is now in a broken state because of code they do not control. They can't actually change the name of the thing, because the conflict is in B.

The whole point is that the names of private fields are something which consumers of your code should not have to know.

Also, I'm not sure it actually is all that different from other languages. What difference do you mean?

lifaon74 commented 7 years ago

Well, let's compare with java which is pretty good for poo :

So # is not really a "private" but more a "class scoped". It's not a modifiers but only a property only available in the class context. So yes, # makes sense in some situations, but in this case it should not be called "private" to avoid confusion for others. This is like the below example but for es6 classes :

function A() {
  let a = 1; // only visible in A
  this.method() {
    console.log(a);
  }
}

So the need for real private, protected and friend is still required and complementary with #.

On your previous argument, you rely on the fact a dev would extend an external lib (this is really rare but could append). If he updates a lib and see that it doesn't work anymore, it's simply what appends for most of the libs updates... he has 2 solutions => rollback or edit it's own classes.

bakkot commented 7 years ago

reflection

It's funny you should mention Java, because they're currently in the process of adding a major new feature to the language (modules) in part because they found that developers needed a way to prevent reflection from accessing private fields.

protected / friend classes

I'm not sure why "some languages also have protected fields and friend classes" means that their private fields work any differently. (And of course not all languages do have friend classes - like Java, for example.) We're not calling this "general access modifiers"; we're calling it "private fields". And as far as I can tell, it pretty much works how private fields work in most other languages (plus or minus reflection, anyway). Why do you think this proposal doesn't count as "real private fields"?

If he updates a lib and see that it doesn't work anymore, it's simply what appends for most of the libs updates... he has 2 solutions => rollback or edit it's own classes.

I don't think this is an acceptable cost.

lifaon74 commented 7 years ago

Well it's strange because you seems to be in a total deny of the importance of "protected", "friend" and reflection : if they were implemented on many languages it was for a good reason.

Modifiers are more metadata to inform the developer (for other languages, the access check is done at compilation time only and not put into the binary code). In js the check could only be done at run time. The # here has not the same "definition/implementation" than in other languages, it's a "class scoped property" and has of course importance for some usecases (so the proposal makes sense), but I would be strongly disappointed if some external lib was "limited/restrained" just because they abuse of # fields (imagine a jQuery without extension, etc...). I just see one bypass way :

import { A } from 'A';

A.prototype.getPrivateX() {
  return this.#x; // pretty ugly according to me...
}

What I want to point is that we need more (as I repeat since the beginning), see further to have real class and solve concrete daily problems. It's not all about modifiers, its too about multiple inheritance and abstract classes : things that exists from ages and could be a real improvement for js.

Jack-Works commented 7 years ago

+1 for thinking this.#x is pretty ugly.

But, the swap example in #14 is also a big problem for using private x. Maybe we have to compromise to the cost (discussed in the #14 ) of determinate which to access when using this.x

In typescript, the private modifier is just a compile-time modifier, private x will still be accessible in the runtime. Although private works well in TS, it's unacceptable to have a "compile-time" private for ES.

I have no idea about other languages which have private as their private class field, maybe these languages have a well-designed mechanism to avoid this problem. But I still prefer private x to #x.

lifaon74 commented 7 years ago

@Jack-Works did you reference the correct issue ?

Jack-Works commented 7 years ago

I'm sorry, I cannot find out which issue I want to refer. But I laterly find that problem is already described in the FAQs

On Tue, 18 Jul 2017, 16:38 valentin, notifications@github.com wrote:

@Jack-Works https://github.com/jack-works did you reference the correct issue ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-class-fields/issues/15#issuecomment-315997364, or mute the thread https://github.com/notifications/unsubscribe-auth/AFJBf7BfDiqyPzWocQjfSrEq8o1cYwdLks5sPG8YgaJpZM4OT5Pp .

rbuckton commented 7 years ago

private x

One possibility to support private x is to leverage something like [[HomeObject]] to perform an access check as part of [[Get]], [[Set]], [[Delete]], etc.

For example, we could modify the behavior of GetValue(V):

It's not terribly expensive, though it expands the definition of [[HomeObject]] a bit. It then aligns more closely with how other languages treat private. With those rules, we can easily expand them to support protected or other forms.

I'm not advocating for this, just pointing out the possibility. I do feel it calls into question the validity of this entry in the FAQ that calls out private x for being slow.

"friend" access

I imagine you can emulate "friend" access, at least within the same file:

let xAccessor;
class A {
  #x
  constructor() {
    // set up "friend" accessor for #x
    if (!xAccessor) xAccessor = { get: (a) => a.#x, set: (a, v) => a.#x = v };
  }
}

class AFriend {
  constructor(aInstance) {
    console.log(xAccessor.get(aInstance));
  }
}
bakkot commented 7 years ago

@rbuckton:

I do feel it calls into question the validity of this entry in the FAQ that calls out private x for being slow.

How so? Adding steps to every [[Get]] is exactly the concern that part refers to.

rbuckton commented 7 years ago

Yes, but without metrics that statement is pure conjecture. I could understand the argument that it would make [[Get]] slow if it was expensive to correlate the caller to the receiver. The changes to the algorithm steps needed to accomplish this seem small enough that the only way definitively state the performance cost is with actual data. We shouldn't make a blanket statement that private x shouldn't be considered in part because it is slow without empirical evidence.

Again, I am not necessarily advocating for private x. I am pointing out that we should avoid superlatives and conjecture when defining an argument for, or against, a specific part of the proposal.

bakkot commented 7 years ago

The FAQ entry just says we want to avoid further complicating property access semantics, which is true, and that adding a runtime typecheck on property access would slow down property access, which I believe is also true. The FAQ entry doesn't say it would be slow for some objective definition of slow, or include any superlatives that I can see. It certainly doesn't definitively state any particular performance cost.

What phrasing would you prefer?

rbuckton commented 7 years ago

I don't have better phrasing to offer at the moment, but would be interested to see what, if any, performance impact such a change would have. I just balk a bit at (paraphrased) "we don't want to do X because its slower" without showing that it's slower when that impact is non-obvious.

lifaon74 commented 7 years ago

Well, for me if the field is public the GetValue should be as fast as before, because only private/protected requires to check the context, Moreover, a private/protected check should not be very slow (probably only 2 times slower than public) because the context (stack) is already known by the environment.

claytongulick commented 7 years ago

I think @rbuckton 's point makes a lot of sense to me in the context of high performance js. What is an acceptable level of performance penalty v/s flexibility, power and future proofing?

The performance concern is certainly something to be aware of, but not necessarily something that should call an absolute halt to alternate proposals like private x or @private x.

Given that we already have to work around js performance issues in tight code anyway. ES5:

function() {
  var x = 1;

  function() {
    function() {
      function() {
        var i=0;
        var inner_x = x; //we always have to do this in tight code today
        var calculated = 0;

        //slow, because of scope chain navigation
        for(i=0; i<1000000000; i++) calculated = i + x;

        //fast, because we've brought the variable into scope
        for(i=0; i<1000000000; i++) calculated = i + inner_x;
      }
    }
  }
}

Pretty much everyone doing high performance js is aware of the above issue. And of course the same thing applies to the prototype chain lookup, and ES6 doesn't really change that nor anything I've read about class fields:

class Foo { x=1;  ... }
class Bar extends Foo {...}
class Baz extends Bar {...}

b = new Baz();
for(let i=0; i<1000000000; i++) b.x + i; //slow because of prototype chain navigation

Again, when you're really concerned about squeezing performance, you always cache the variable you're accessing.

Given the penalty of prototype and scope chain navigation, @rbuckton makes a strong point about quantifying the performance impact of alternate approaches - we already have to deal with performance penalties for having the power of prototypal inheritance and closures, and we gladly accept them and work around them when necessary. The same patterns and workarounds would still apply to access modifiers, i.e. local value caching, so I don't really see the problem.

bakkot commented 7 years ago

@claytongulick, performance is not the only consideration mentioned even in that single FAQ entry. At least as serious is the other issue raised, i.e., that a given piece of code might act on a either a private field or a public field depending on the type of the object passed in.

bakkot commented 7 years ago

Aside: your comment about your ES6 code is mistaken, if I'm reading it right; class fields are always installed in the instance being constructed. So x would be an own property of b in your code, and modifying it would require no prototype chain navigation.

rbuckton commented 7 years ago

The other issue raised is the exact nature of accessibility in other languages. Whether or not a given piece of code can act on a public or private field depends on whether the type of the object passed in permits it. This is not much different than checking whether that object has set the property to be [[Writable]]: false.

That said, there is a more complex issue with private x when it comes to prototype chains that likely can't be resolved:

class C {
  private x = "secret";
  verify(secret) {
    return secret === this.x
  }  
}

// or use a Proxy...
const obj = Object.create(new C, {
  x: { value: "broken" }
});
obj.verify("broken"); // true

Possibly not insurmountable, but is a definite benefit of #x due to lexical scoping.

lifaon74 commented 7 years ago

For me { x: { value: "broken" } } is an object with a public property x. So :

const obj = Object.create(new C, {
  x: { value: "broken" }
});

Will convert the private property x to a public property like

obj = new C();
Object.defineProperty(obj, 'x', {
  value: 'broken',
  modifier: 'public'
});

In your example, I guess you're trying to show that a parent class cannot have a distinct field name x than a child class with a property x too. Yes, totally agree. As mentioned earlier, the private I propose it not the same as the # that I prefer to call "class scoped property". For me a private could be editable to allow in some case to improve performance, fix bugs, etc... So in your example, the developer is aware than a private x exists and voluntary change it to a public property. The deal is the same we have since the beginning : if you extend a class you should take care not to overload preexisting properties (the IDE could help to detect this).

claytongulick commented 7 years ago

@bakkot just trying to address issues one at a time - performance is an oft-repeated one, and worthy of discussion and exploration. Given that we have ways of addressing the performance impact of alternate approaches (variable caching, etc...) perhaps we can agree that it isn't a primary selling point of #?

Also - thanks for setting me straight on class fields, I'm so used to the way ES5 works, I just assumed with 'maximally minimal' that class fields would desugar into the way things had always been done. Nice to learn that's not the case.

mbrowne commented 6 years ago

Apple wrote a good blog post arguing why protected access is of limited value: https://developer.apple.com/swift/blog/?id=11.

The blog post is about Swift, but I think the principles they bring up are applicable in general.

protected is very much tied to inheritance and class-oriented thinking as opposed to true object-orientation. However, I agree that if we don't have protected, we need something to fulfill a similar role in allowing access to members outside of a class without necessarily making them public everywhere.

I like Apple's solution of internal access, which could also be used to implement something like friend classes. But I realize that this might complicate things in JS, since currently there isn't really module or library scope (once you export something, it can be imported from anywhere).

lifaon74 commented 6 years ago

Well, I agree on some points: The best of the best would be to have the notion of "package" : a bunch of files (scripts) which have access to any method/attribute/function but only some are exposed outside of the package. Something like:

class A {
  // visible outside of the package
  exposed a() {
    // ...
  }

  // public internally of the package but not visible outside
  b() {
  }
}

This is exactly what does the js vm : having some internal scripts and expose only some classes and methods, this is a really nice design which allow the lib developer to have full access on its internals properties but only expose "simplified" and safe version of the classes.

This kind of thing is "impossible" in js (too many changes/restriction) but could be reached through a transpiler and some native private/protected/friend properties.

ljharb commented 6 years ago

The real problem I see is that in many languages, "private", "protected", and "public" are not actually access specifiers - they're friendly suggestions (that the language often respects in not-all of its APIs).

"private" in JS, thankfully, is actually specifying access - you can explicitly expose a private field, but by default, it's impossible to reach.

So far, I've not seen a "protected" or "package" or similar proposal for JS that would, by default, be impossible to subvert (it ofc is fine if an author explicitly exposes anything). What might that look like? I don't think it's productive to talk about any implementation that lacks that property (others may disagree, ofc).

jkrems commented 6 years ago

If you want to have internals & an exposed interface in the same class, it's likely that your implementation and interface types are bleeding into each other. A minimal (and oversimplified) solution for JS would be:

class Interface {
  #impl;

  /* "private" */ constructor(...args) {
    #impl = new Implementation(...args);
  }

  a() {
    return #impl.a();
  }
}

Added benefit: You don't have to worry about test visibility and similar hacks.

lifaon74 commented 6 years ago

@ljharb Yes modifiers are more informations to de developer, not really something to constrain him. If we want to bypass them, their is always a solution. So for JS, this could be by assigning modifiers through Object.defineProperty. As I mentioned earlier, the # is not really a private, it's more a "class scoped property" => something impossible to reach outside of the class except if exposed (with getter for example). In other languages, a private is still accessible with Reflection, which in some case is really useful. "class scoped property" and "private" properties are different and complementary things. I'm just afraid that some developers would abuse of the # thinking it as a private like in other languages, and being limited by not having the power to access a # property to fasten code execution.

Imagine for example an implementation of the Blob containing a #buffer property. The only way to read a Blob is to use a FileReader which is far far slower than a direct access of the buffer. This is to avoid this king of "non optimized" development that I would prefer private/protected with the possibility to modify them if necessary.

@jkrems Yes, solutions exists but nothing really native, so nothing really fast... And, most important, it doesn't follow the DRY rule. If we need getter and setters for every # that we want to expose. A keyword would be more elegant.

ljharb commented 6 years ago

@lifaon74 the current private fields proposal for JS is indeed private, and offers no reflection mechanisms to get at the values nor even determine they exist.

rbuckton commented 6 years ago

In C# 'private', 'protected', and 'internal' do specify access, though there are reflection mechanisms that can be used to violate access but only in Full-Trust environments. Partially trusted code has no access to non-public members even through reflection.

mbrowne commented 6 years ago

Let's consider an example. Suppose we have an Event class with multiple subclasses, one of which is MusicEvent, and there's a protected modifier available to us:

class Event {
    title;
    startTime;
    endTime;
    categories = [];

    findRelatedEvents() {
        return buildRelatedEventsQuery().fetch();
    }

    protected buildRelatedEventsQuery(eventType = null) {
        //construct a query to find related events
    }
}

class MusicPerformance extends Event {
    performers = [];
    standardTicketPrice;

    findRelatedPerformances() {
        return this.buildRelatedEventsQuery('MusicPerformance').fetch();
    }
}

I consider this a perfectly valid use of inheritance, but it would be equally valid (if more verbose) to use embedding/forwarding instead:

class MusicPerformance {
    performers = [];
    standardTicketPrice;

    constructor() {
        this.event = new Event();
    }

    get title() {
        return this.event.title;
    }

    set title(title) {
        this.event.title = title;
    }

    get startTime() {
        return this.event.startTime;
    }

    set startTime(startTime) {
        this.event.startTime = startTime;
    }

    //etc.
    ...

    findRelatedPerformances() {
        return this.event.buildRelatedEventsQuery('MusicPerformance').fetch();
    }
}

And of course there are more standard uses of composition that could be better demonstrated with other examples.

My point is that the real intent behind protected members is to allow multiple classes to access shared internal state and behavior; there's no need to conflate it with inheritance. And if it's not feasible to have an internal access modifier relying on some sort of internal library scope, then we should at least have something like friend classes. JS programmers have never relied solely on single inheritance to share properties and behavior between tightly related units of code, so private and protected alone would be insufficient.

ljharb commented 6 years ago

@mbrowne how do you suggest class A and B be "friends" such that they can share protected items without being defined in the same scope, but have it be impossible under any circumstances including reflection for class C to determine any information about this friend/protected relationship?

mbrowne commented 6 years ago

@ljharb Ideally there would be a way to specify that two or more files belong to the same internal scope.

But let me back up for a minute and say that in my view, the most important purposes of access modifiers are to (1) document intent and (2) make it difficult to circumvent it. I don't care that much about it being truly impossible, although I think having the ability to create truly private properties that are totally invisible outside a class is certainly useful. (It also honors Alan Kay's original ideas about encapsulation, with the idea that objects are like a mini-computers "all hooked together by a very fast network", or like biological cells in an organism.) But having worked on some ORM libraries in the past (albeit not for JS) I also understand the value of reflection, and I think the Python philosophy of "we're all adults here" makes a valid point as well. So I can see both sides of the debate... Also, I'm more concerned about the ability to strictly enforce encapsulation at the class level than at the "friend class" level, since we already have the ability to package a module and use it via node_modules (or equivalent) and only expose what we want at the top level.

lifaon74 commented 6 years ago

@ljharb : you seems to really believe in the # properties here are the concrete problems they don't cover : If you have a proper, fast and elegant way to solve it, feel free to share :

As I said, the modifiers are here to inform and protect developers, not limiting them. An private or Object.defineProperty('a', { modifier :'private' }) could hide totally a property ensuring some privacy. But would allow tier developers to dig into the source code, find that some properties are private and modify them with some friend or Object.defineProperty('a', { modifier :'private', friend: fnc) or some protected Object.defineProperty('a', { modifier :'protected')

ljharb commented 6 years ago

@lifaon74 You can already achieve both "friend" and "reflection" with Symbols.

To me, the entire point is to limit them - I want to absolutely prevent anyone from doing things with my private data at runtime; anything less isn't "private", it's just "please don't touch". Digging into the source code isn't what I'm interested in; making runtime detection impossible is.

bakkot commented 6 years ago

@lifaon74

friend

Friend classes are useful in a number of styles, though hardly essential - Java continues to thrive without them, whatever pain your friends may have experienced. They're still something we're thinking about, but I don't think their absence should block this feature.

In the mean time, they can be simulated with this feature reasonably well:

class PrivateReader {
  static #read;
  static setReader(fn) {
    this.#read = fn;
  }
  implementation(o) {
    doSomethingWithPrivateData(PrivateReader.#read(o));
  }
}

// somewhere later, possibly even in another module...

class PrivateImplementation {
  #myData;
  static read(o) {
    return o.#myData;
  }
}
PrivateReader.setReader(PrivateImplementation.read);
delete PrivateImplementation.read; // or leave it, if you want to allow reflection

reflection

Yes, there's a fundamental conflict between reflection and encapsulation. We recognize that there are benefits to allowing reflection. But, ultimately, after a great deal of discussion, conversation with library authors and consumers, and considerations of what's happened in other languages (like Java, as I mention above), we came down on the side of encapsulation.

This doesn't mean reflection isn't valuable. But we can't have both: either private fields are private, or they aren't. I don't think there's much more to say on the matter.

lifaon74 commented 6 years ago

@bakkot Well thanks for your example pretty cleaver. The only "problem" I see is that it's quite verbose, which doesn't follow the DRY rule because you need getter/setters and initializers for a lot of properties instead of a keyword like friend. Except that (which could cost time dev + maintenance to companies :'( ), it's a valid solution.

I already participate to some discussion and specs meetings (including IRL) and I know that most of the time, decisions are done by only a few peoples (29 participants in the discussion linked). Same the the deprecate, sometime only initialized by one guy and adopted without deep discussion. Moreover, its probably a biased sample, because it includes peoples from IT and from github community but excludes IT managers, foreign dev (not necessary speaking english) and a lot of other developers not interested to help in the specs... Before adopting # we should really have metrics like : "how many people use WeakMap to have private property (exact behavior as #), what are the problems they encounter, etc..."

The problems I listed here are problems I personally encountered while using WeakMap as "private" properties. It's cool for you (the lib author) because you have full control over the visibility but a pain for your mates which refactor them every min to change it to public because in some circonstances they need access to a "private" from another classes/function even if it should not be exposed outside of the package.

@ljharb

You can already achieve both "friend" and "reflection" with Symbols.

Yes, this is currently what I'm doing, but I find it less elegant that a friend.

Moreover, guys, because you seems to be really involved, don't hesitate to share your feelings about multiple inheritance too. As you mentionned maybe not the best place to speak about it, but it's a least a start point.

mbrowne commented 6 years ago

Probably the best argument in favor of truly private properties is that in some cases they can prevent security holes. And in general I think true encapsulation makes sense as the default, but maybe there could be some convention for creating "private" properties that can still be accessed somehow, e.g. using an internal modifier (if that's viable) but prefixing with an underscore to indicate that it shouldn't be used outside the class unless there's a really special reason.

Of course that wouldn't fully address what @lifaon74 said about library authors not anticipating circumstances where something private needs to be accessed...currently it's not that uncommon to access properties prefixed with _ in a 3rd party library if the only other solution to a particular requirement would be to fork the whole library. But it's probably not a good idea for the language to enable people to hack things, especially things that were meant to be internal only to one class. Perhaps this will encourage the community to solve these situations in a better way, by properly modifying the library to be more extensible (most popular libraries are open-source, after all).

jeffmo commented 6 years ago

Hey all, some really great discussion and feedback here and I've enjoyed following this discussion.

I just wanted to chime in to add that I, myself, think there may be an argument for including some concept of "friends" and/or possibly even reflection of some kind in the future; however, doing so now would be a bit premature. Historically when proposals at TC39 have been controversial or new/unfamiliar (in the context of JS), we've opted to stick to a minimum viable product for the first version of a feature and leave room to iterate/extend later with subsequent proposals. That seems like the right thing to do here while we familiarize ourselves with the idioms and patterns that arise for hard-private fields from both the broader community and personal experiences over the next few years.

Please don't stop discussing the merits of these things here -- it's all really useful discussion -- I just wanted to set clear expectations for this particular proposal at the moment.

claytongulick commented 6 years ago

@jeffmo I think your comment illustrates exactly my concern with the current proposal. The private field syntax as defined is controversial, exactly 100% of the js devs I've discussed it with dislike the syntax. These aren't standards developer types of developers, these are day to day developers writing applications, across several different companies and teams. Most of these folks feel pretty uncomfortable participating in a discussion like this, because they don't want to accidentally say something publicly that's perceived as being bone-headed.

I don't think that the # syntax qualifies as a minimum viable, it's a pretty bold use of the hash mark that, once done, is irreversible - toothpaste won't go back into that tube - and has a lot of resistance from day to day devs that I've talked to, even though I can understand and appreciate the technical reasons behind it (thanks to @bakkot for patiently helping me understand and exploring other options).

I can see us (as a community) potentially kicking ourselves for burning this character to solve a problem for which there are already alternate solutions.

For example, given that we, as js developers, are trying to build and enhance the open web, how cool would it be to have a new structured metadata type comment that uses # for 'hashtagging' code twitter style? We could convert unminified js and sourcemaps to a publicly searchable database of hashtagged code snippets, and suddenly being a good js citizen involves hashtagging your code so that others can search it and learn from it... this is just one example off the top of my head. Perhaps it wouldn't conflict syntax wise with privates, but you get my point? Do we really want to be quite this aggressive with using the # character for such a controversial feature? Especially when other solutions exist?