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

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

Closed
lifaon74 opened this issue Jul 11, 2017 · 168 comments
Closed

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

lifaon74 opened this issue Jul 11, 2017 · 168 comments

Comments

@lifaon74
Copy link

lifaon74 commented Jul 11, 2017

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 (tc39/proposal-private-fields#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

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

  2. 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 :

  • supers keyword to init many classes at once : supers(params class B, params class C), ...
  • some kind of Reflect.super(methodName[, superClass]) to allow apply / call on the super<B>().

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

bakkot commented Jul 11, 2017

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

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

  • allow access to private members from a "friend" class or subclass (with protected members). Strongly required for classes from a package/module which could access private members from each others, but don't expose them when exporting.
/**
*  In this example, the class B is exported but its `attr` is hidden when importing B
**/
class A {
  readB(bInstance) {
    // class A could perform some complex algorithms on B private members
    console.log(bInstance.attr);
  }
}

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

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

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

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

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

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

lifaon74 commented Jul 14, 2017

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

claytongulick commented Jul 14, 2017

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

bakkot commented Jul 14, 2017

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

claytongulick commented Jul 14, 2017

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

bakkot commented Jul 14, 2017

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

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

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

bakkot commented Jul 15, 2017

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

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

  • private and protected can be accessed through reflection (really useful in some situations). Rare case but could append =>the main 2 reasons : old lib no more supported with bug, or performance improvement (access a private field instead of some method can strongly increase performances in some cases)
  • protected can be accessed for child classes (really important)
  • in C, we can use "friend" to allow some classes/function to access a property

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

bakkot commented Jul 15, 2017

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

lifaon74 commented Jul 15, 2017

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.

  • The "protected" to allow child class to access protected member in order to hide a property to external classes but allow developers fast access for some properties. It's an equivalent of "friend" for any classes which extends the mother.
  • The "friend" to allow for example classes from a same package (a collection of module) to access directly to a private/protected member to hide some underlying process. A concrete example could be the implementation of the web-stream (standard, implementation) : the ReadableStreamDefaultReader modify the internal "private" properties ReadableStream and hide to the final user a lot of underlying process or unnecessary properties. Without the use of "private" and "friend" the properties can only be public and visible by the end user. In many cases classes into a package communicates with each other, but don't want to expose the private fields outside of this package. Moreover, I have a lot of coworkers working on Java and they often complains of he lack of "friend" and are forced to do "ugly" fixes to access to private members.
  • Finally, reflection has being introduced to allow fine control over classes and bypass some mechanism in rare situations. For example, with the introduction of web-component a fast problem occurred : its impossible to instantiate an HTMLElement (so extending WebComponent by any HTMLElement was impossible except with es6 classes), to patch this the Reflect.construct was introduced to allow the construction of such an element. So I strongly anticipate this kind of same requirement for # fields.

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

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

@Jack-Works did you reference the correct issue ?

@Jack-Works
Copy link
Member

Jack-Works commented Jul 18, 2017 via email

@rbuckton
Copy link

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):

  • Let home be the [[HomeObject]] of the current Environment Record
  • Calls base.[[Get]](GetReferencedName(V), GetThisValue(V), home)
  • [[Get]] checks if P is declared private on O. If so, compare home to the object that declared the property.

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

bakkot commented Jul 22, 2017

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

rbuckton commented Jul 22, 2017

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

bakkot commented Jul 22, 2017

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

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

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

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

bakkot commented Jul 22, 2017

@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.

@mbrowne
Copy link

mbrowne commented Aug 18, 2017

In case anyone here is interested, I raised some other concerns related to reflection in #36 (and in an earlier issue, #33). The discussion reached a conclusion, so I closed both issues, but only a few people participated and I thought I'd write a quick note here in case anyone else has input.

@spencerhakim
Copy link

Using decorators for protected fields doesn't feel right to me. If this is still in the proposal stage, then why wouldn't the goal be to get this right the first time around instead of waiting for another proposal to hopefully improve/fix things later on? That said, I'd prefer something along the lines of:

class A {
  ##privateField = 0;
  #protectedField = 0;
  publicField = 0;

  constructor() {
    ##privateField++;  // ✅
    #protectedField++;  // ✅
    publicField++;  // ✅
  }
}

class B extends A {
  constructor() {
    super();

    ##privateField++; // 🚫
    #protectedField++; // ✅
    publicField++;  // ✅
  }
}

The #-prefixes could be swapped for private and protected to match the current proposal, but I think it looks more logically consistent this way. It could be argued that ## looks a bit verbose, but it's certainly less verbose than a @inheritable/@inherited pair (and not to be pedantic, but using decorators for what should be a basic language feature reminds me of Java's odd use of semi-required annotations for overriding methods).

As for whether access to the protected field is controlled through something like the private fields' internal slots list, or by simply checking the prototype chain, I leave that entirely up to others.

@bakkot
Copy link
Contributor

bakkot commented Sep 1, 2017

@spencerhakim, why? There are many possible levels of access control between "fully public" and "fully private", including probably some which we haven't even thought of; I don't understand the desire to give language support to protected in particular - especially if it doesn't add any actual guarantees, as noted above.

@spencerhakim
Copy link

(Forgive me if I'm missing something here, did my best to read through this thread, but there's quite a bit)

@bakkot I understand and agree that many intermediate levels of access control can and do exist, but from what I've read, the ones discussed here can generally be grouped together as follows (with keywords loosely based on your examples):

  1. Class fields exposed to subclasses through inheritance (protected)
  2. Class fields exposed to other classes/functions (friend)
  3. Class fields exposed on some module/package level (internal within own module/package, friend otherwise)

I believe 2 is out of scope for this proposal, and 3 is likely out of scope for the language (considering the various module systems currently in use), and both of them could have solutions that apply not just to class fields, but to methods and classes/functions/variables (on a module/package level), too. Even if an implementation for 1 can't make guarantees in situations where the instance's [[Prototype]] is changed (I think that's what you were referring to?), that's fine; it's not like messing with [[Prototype]] isn't strongly discouraged as it is.

If @ljharb / the committee decides that this proposal should only deal with the object-oriented aspect of JS and not the class/prototype-oriented aspect of it and continues with this proposal as-is, then so be it, but (considering this proposal is about Class Fields, after all) I'd be disappointed to see developers left with only an all-or-nothing approach to field access until some indeterminate point in the future when a proposal that likely deals with a much wider span of access levels is standardized.

@mbrowne
Copy link

mbrowne commented Sep 1, 2017

I too would like to see a more complete native solution for access control that doesn't rely on decorators. But that's a much more complex problem than just private fields, and I think it should be done with more community feedback than has been obtained so far or could be obtained just from this github thread. And I agree with what @bakkot said earlier in this thread:

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.

Allowing people to play with private fields and decorators would be a great way to get more feedback and real-world experience with other access levels.

Designing a native solution right off the bat for every access level deemed necessary would certainly slow down the ratification of this proposal, and people have been asking for private fields for a long time. I do think it's important that we not paint ourselves into a corner by introducing private fields in such a way that would limit us in the future, but we discussed that already and I think the current proposal leaves many options on the table for future native access control features.

I might be more inclined to agree with @spencerhakim if protected were more important than friend (or internal, if that were viable) and simple to implement. But given that in many cases composition is preferable to inheritance, I would say friend is just as important, so it doesn't really make sense to add support just for protected and still be left with an incomplete access control solution. (And with the introduction of decorators, polymorphism in classes will be less tied to inheritance than it is now.)

@anjmao
Copy link

anjmao commented Jan 22, 2018

@littledan
After reading FAQ and all discussions I still don't understand why we can't just use Typescript private, protected access modifiers implementation which is by far the best. I hope you can answer these questions and give more low level details.

Why aren't declarations private x
This sort of declaration is what other languages use (notably Java), and implies that access would be done with this.x. Assuming that isn't the case (see below), in JavaScript this would silently create or access a public field, rather than throwing an error. This is a major potential source of bugs or invisibly making public fields which were intended to be private.

If I mark field as private how can it silently become public? Can you give some lower level details (V8 js engine) what is wrong with using private keyword and this?

class Foo {
   private _name = 'bar';

   get name() {
       return this._name;
   }
}

const foo = new Foo();
foo._name // browser should throw error
foo.name // ok

Why isn't access this.x
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.

Less significantly, this would also break an invariant of current syntax: this.x and this['x'] are currently always semantically identical.

Why we need to have private and public fields named the same x (it is not possible). Just name you private field with underscore _x as this is in general common practice. Also I don't understand why this.x and this['x'] plays any role here. In any way browser js engine should have implementation where it can track which field is private. Naming private with # just simplifies implementation a bit. Here I also missing low level details.

  1. If we have private we also need to have protected to be able to access field from base class. Without it private fields proposal doesn't make sense. Is it?

Thanks.

@lifaon74
Copy link
Author

lifaon74 commented Jan 22, 2018

Well, I'll try to resume :

  • Yes, for me private, public, protected (and friend) are highly wanted by many developers and the
    #private divide the community => it's not a consensus.
  • The proposal #private hasn't the same meaning it has in other languages. Here (the proposal) it's a scoped property: a property only accessible by the class like:
// module A.js
const _private = 'private';

export class A {
  method() {
    console.log(_private);
  }
}

In other languages, private are not scoped like this and can be accessed through reflection. So the term "private" for this proposal is wrong. This should be called: "scoped properties" instead.

@mbrowne
Copy link

mbrowne commented Jan 22, 2018

@lifaon74 Yes, this proposal is for what people have been calling "hard private" instead of "soft private" - so private properties would be truly inaccessible from outside the class (not even via reflection). To be clear, unlike your code example, private properties would still be specific to each instance (except for private static properties).

@anjmao the Typescript private modifier is definitely not "the best" when it comes to true privacy. Of course reflection is commonly very useful as well, and the decorators proposal is keeping that in mind - so that decorators could be used in conjunction with hard privates to provide reflection (i.e. turning them into "soft privates"). Personally I hope that many library authors will take advantage of that for any internals that are unlikely to change over time, rather than automatically using hard private for all internals -- which could be limiting in cases where they don't anticipate the ways some people might need to modify or extend functionality for edge cases. But in any case I do see the value of having hard private for cases when you really want to forbid outside access to your property, and this is not possible with current ES.

Regarding protected access, yes that is an important use case (or at least something similar to it), and this proposal leaves open many possibilities for introducing protected in the future. It will also be possible to implement it using decorators. Protected/friend/etc were discussed at length earlier in this thread.

@lifaon74
Copy link
Author

lifaon74 commented Jan 22, 2018

I forget to mention it but YES, #private (hard private) makes sense allowing cool stuff. I just hope developers won't abuse of it: "with great power comes great responsibility". The term "private" in this proposal add just many confusion vs other languages private meaning. A "class scoped property" makes more sense.

@mbrowne
Copy link

mbrowne commented Jan 22, 2018 via email

@anjmao
Copy link

anjmao commented Jan 22, 2018

@lifaon74 @mbrowne Thanks for your answers. I still need few more to be 100% clear.

  1. So if I have class
class Foo {
   #name = 'nooo - this is hard private';
}

const foo = new Foo();
foo.#name;

What will happen then I run this script in the browser at runtime? Will it just throw SyntaxError or something like OhNoPrivateWasUsedOutsideClassError?

  1. Is it true what hard privates can't be implemented like in Typescript using just private keyword because developers will think what this is something similar to already known languages like C#, Java, PHP and it means soft private, but they will not be able to access it even through reflection if they need it for some corner cases, but if they want they can create decorator which will still transform hard private to soft private.

  2. But isn't the problem just in lack of visibility. We all know that in language like PHP you can describe class visibility like this

class MyClass
{
    public $public = 'Public';
    protected $protected = 'Protected';
    private $private = 'Private';
}

$obj = new MyClass();
echo $obj->private; // Ups

and at runtime you will get error

Uncaught Error: Cannot access private property MyClass::$private

So what is the reason we can't simply have public, private, protected in ES class

class MyClass {
    public publicVar = 'Public';
    protected protectedVar = 'Protected';
    private privateVar = 'Private';
}

const obj = new MyClass();
console.log(obj.privateVar); // Ups. Should throw error at runtime

Normal library user will not be able to access any internals because tooling will show errors and JS runtime will throw error at runtime so we are all good and safe in 99% cases. I still want to access all private properties if I want using reflection (I'm advanced user), but this should be my own responsibility.

  1. is it true what one of reasons to use #private is this
class A {
    private privateVar = 'Private';
}

class B {
    giveMeA(a) {
         a.privateVar; // will throw error, but I will know it only on runtime because javascript is dynamic language and I need to use Flow or Typescript to annotate argument type.
    }
}

const a = new A();
const b = new B();
b.giveMeA(a); // throw error at runtime

so by having you private named with # prefix it is more clear and tooling can figure it out. In any why smart tooling will be able to detect such cases.

@jkrems
Copy link

jkrems commented Jan 22, 2018

@anjmao The problem with throwing (was pointed out in some comments around this repo and might also be in the FAQ): It makes it unsafe and a breaking change to add a private field to a class. Which this proposal doesn't consider acceptable. Example:

class AppClass extends LibClass {
  constructor() { this.name = 'foo'; } // better hope `LibClass` never adds a `private name` field!
}

If we'd allow naming collisions between public and private fields, then the library implementing LibClass could never add or rename any private fields without bumping the major version. Which seems weird for something that's supposed to be private and hidden from users of the class. You can work around it by naming each private field ___my_lib_2018_01_15__name__ and hoping for the best but that doesn't seem to be a great solution.

@trusktr
Copy link

trusktr commented Mar 21, 2018

About multiple inheritance,

but can only be archived through mixins or factories

It can also be achieved with prototype chain branching using Proxies, and a helper that puts it all together.

Usage is like this:

import multiple from 'path/to/multiple-helper.js'
class Foo {...}
class Bar {...}
class Baz extends multiple(Foo, Bar) {...}
console.assert( ( new Baz() ) instanceof Foo ) // works
console.assert( ( new Baz() ) instanceof Bar ) // works

I have a couple non-proxy implementations in these gists:

  1. https://gist.github.com/trusktr/05b9c763ac70d7086fe3a08c2c4fb4bf (attempts to branch prototype reads, like we would with Proxy, but without Proxy. Works with super.)
  2. https://gist.github.com/trusktr/8c515f7bd7436e09a4baa7a63cd7cc37 (this one doesn't work very well, and in fact the result is a flat chain which is the same as you can do with class-factory mixins. It can probably be made to work with some hacks, but probably not worth it)

I hadn't made the Proxy implementation yet, because Proxy wasn't widely supported at the time, but I'd like to revisit it and make that in implementation 3.

I'd to add the Proxy implementation as an additional feature for https://github.com/trusktr/lowclass.

Just sharing here, as I'm interested in this stuff for fun (and, well, because I also feel that having protected and private members can help make code more solid). 😄

@rdking
Copy link

rdking commented Jun 15, 2018

I just realized the flaw in one of the arguments about encapsulation...

On July 14, 2017, @bakkot wrote:

@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?

As it works out, there's no conflict here.Base.m(new Derived()); returns 1. Even without private fields, it would still return 1. You can test this for yourself right now.

class Base {
   static m(obj) {
      return obj.x
   }
   static m2(obj) {
      obj = (typeof(obj.__proto__) == "object") ? obj.__proto__.__proto__ : obj;
      return obj.x;
   }
}
Base.prototype.x = 0;

class Derived extends Base {
}
Derived.prototype.x = 1;

var d = new Derived();
Base.m(d); // returns 1;
Base.m2(d) //returns 0;

This is how it has always worked in ES. I don't see why declaring x as private in class Base would make a difference.

The mistake in your logic is that you applied the reasoning for a language with hard types to ES which is duck typed. There is no confusion precisely because ES doesn't have hard types. In the end, because class is just sugar for a factory function that forces you to use new, which itself is approximately sugar for type.apply(Object.create(type.prototype), arguments), and sticks it public members in the prototype of the class's constructor function, there will never be any confusion over what's being accessed for declared members of a class and derived classes. This is already as it should be, even for private members.

@ljharb
Copy link
Member

ljharb commented Jun 15, 2018

As we’ve discussed elsewhere, class is not just sugar (even if it’s mostly sugar), and that isn’t part of its design. It’s a different construct and it will create a different mental model than the outdated ES5 way.

@bakkot
Copy link
Contributor

bakkot commented Jun 15, 2018

I don't see why declaring x as private in class Base would make a difference.

The mistake in your logic is that you applied the reasoning for a language with hard types to ES which is duck typed.

If we accepted this, there would be no point in having private fields at all. Public fields are already adequate for duck-typing. But sometimes you want closure-style encapsulation instead.

@littledan
Copy link
Member

I think the above thread adequately covered the issues with the above proposals. To summarize, a design goal of the class fields proposal is to provide a strong encapsulation boundary, but it seems like the above proposals would not be able to provide such a boundary.

@trusktr
Copy link

trusktr commented Aug 3, 2018

provide a strong encapsulation boundary

Can you make some bulletpoints of the requirements?

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

No branches or pull requests