tc39 / proposal-private-fields

A Private Fields Proposal for ECMAScript
https://tc39.github.io/proposal-private-fields/
320 stars 19 forks source link

Why not use the "private" keyword, like Java or C#? #14

Closed melanke closed 7 years ago

zenparsing commented 8 years ago

Consider this scenario:

class X {
    private a;
    constructor(a) { this.a = a }
    swap(otherX) {
        let otherA = otherX.a;
        otherX.a = this.a;
        this.a = otherA;
        return otherA;
    }
}

Let's call swap with an instance of X as the argument:

let x1 = new X(1);
let x2 = new X(2);
x1.swap(x2); // --> 2

In this case, the reference to otherX.a should access the private field named "a", agreed?

What if we call swap with an object which is not an instance of X?

let x1 = new X(1);
let obj = { a: 3 };
x1.swap(obj); // --> TypeError or 3?

Without a type specifier on the otherX parameter, we don't have any way to tell the compiler whether otherX.a means:

We could just say that within the body of X, property lookups for "a" always refer to the private name, but that would result in surprising behavior in many cases. (Let's come back to this possibility later, though.)

That's why we need to syntactically distinguish between private field lookup and regular property lookup. There are a couple of different ways that we can distinguish them:

I think the second option is the most natural. Unfortunately, either option uses up one of the two last remaining operator characters (the other being the ugly-duck #).

I hope that helps explain the rationale. I'll follow up with a comment exploring the other option that I mentioned above.

yanickrochon commented 8 years ago

I actually don't see anything wrong with the example you provided, even after reading a few times, there's nothing worng with using private (a reserved keyword already).

Consider the last snippet

let x1 = new X(1);
let obj = { a: 3 };
x1.swap(obj); // --> 3

In the code, since it's all being used inside the class and either the private a property of the object's a is accessed, there will be no access violation, therefore the code would not make any difference if a is private or not on the passed argument.

However, if such function would be given :

function swapA(a, b) {
  let tmp = a.a;
  a.a = b.a;
  b.a = tmp;
  return tmp;
}

Then a TypeError would be thrown if passing an instance of X as a.a would fail for a private property not being accessible.

You are not giving a convincing example to reject this proposal, you are just assuming that there should be a difference in the interpretation of this.a if a is declared as private.

The only valid argument to reject private a; is that it would confuse people into knowing what is private and what's not. Then again, since the property is private, it should not be enumerable anyhow, so would not be exposed externally. And we go full circle.

In the README, you are giving parsing directives, but do not describe how these properties are represented at run-time. Currently, properties are defined as enumerable (dfault true), configurable (default true), writable (default true), or with a getter and/or a setter. It should be fairly easy to assume that introducing property visibility would require adding more definable attributes, such as accessible (default true).

zenparsing commented 8 years ago

@yanickrochon If I understand your proposal correctly, you'd change normal property lookup to something like this:

Given obj.prop:

  1. Look up the "prop" named property in obj.
  2. If it exists and is a "private" property:
    1. Check the lexical scope of the current execution context to determine whether the access is allowed or not.
    2. If it's disallowed, then throw a TypeError.
  3. Return the property value, or undefined.

I believe this idea was considered early on and rejected, although the rationale probably isn't captured anywhere.

Think of what you might need to do to make that work.

You could perhaps come up with answers for all of these questions (except for the last), but you'd end up with something highly complex and confusing.

Instead, we're going to use the idea of private slots, which are completely different from JS object properties, and we're going to use a new slot lookup syntax. It's all very simple and easy to think about. And it requires no essential extension to the JS object model. Built-in JS objects like Promise are already specified to have just the same sort of private slots that we are proposing here. We're just providing a way for classes written in JS to have the same kinds of slots.

Also, see https://github.com/zenparsing/es-abstract-refs/issues/11 for more information on why we don't want to use a "private" version of Symbols here.

yanickrochon commented 8 years ago

Yes, you got what I meant right. And I do understand the extra overhead with adding extra property definition attributes. While I know JavaScript quite well, I have not worked under the hood, on the engine itself, so I am not fully aware on how it is actually implemented in details. I was sure that it was possible to tell if a given property was being accessed from within the same context where it was declared without relying on the property's definition, just like let a; cannot be accessed outside it's parent code block.

How do you prevent leaking the names of private fields to clients that shouldn't know that information? This is probably a fatal information leak.

Well, yes, however pretty much any information can be known through "reflection" anyway, even if not exposed or allowed direct access. Java, for example, makes it possible to modify private fields through reflection... While this is not a good thing, it is nonetheless possible and does allow better DI. I am not a great fan block-boxing everything, since JS is not compiled and anyone have access to source code anyway. Makes the effort pointless for run-time. The idea of "private" properties is not essentially to hide them, because this can already be achieved through enumerable: false, but to prevent modifying them outside of it's declaring context. In essence, allowing getting or setting values to a private property depends if this instanceOf PrototypeOfPrivateAttribute.

Classes are supposed to be a semantic feature and should not de-nature the language itself with OO paradigm.

zenparsing commented 8 years ago

The idea of "private" properties is not essentially to hide them, because this can already be achieved through enumerable: false, but to prevent modifying them outside of it's declaring context.

Says you! : )

One of the explicit goals of this private state proposal (or any other that has been brought forward) is to allow the creation of "defensive classes", which are indeed secure at runtime. As a subgoal, we want to allow things like DOM classes (and JS built-ins) to be expressible in JS itself.

melanke commented 8 years ago
class X {
    private a;
    constructor(a) { this.a = a }
    swap(otherX) {
        let otherA = otherX.a;
        // if otherX.a is private then otherA is undefined
        otherX.a = this.a;
        //if otherX.a is private it will create another variable named a only for this scope
        this.a = otherA;
        //if otherX.a was private then this.a is undefined now
        return otherA;
    }
}

let x1 = new X(1);
let obj = { a: 3 }; //a is public here, isn't possible to define private fields this way
x1.swap(obj); // --> 3
glen-84 commented 8 years ago

This looks awful. I associate hash symbols with commented-out code. JavaScript has quite a clean syntax – please don't ruin that.

C++, C#, Java, PHP, TypeScript (and probably others) all support private without an ugly character prefix.

I can't comment on the technical details of the implementation, but there must be a way of solving this in an elegant and efficient manner.

zenparsing commented 8 years ago

@glen-84 Thanks for commenting. I agree that a leading hash looks terrible. We can't use a private keyword, but maybe there are other leading characters that would look better. What do you think about @?

glen-84 commented 8 years ago

What do you think about @?

At signs are already "reserved" for decorators, so you'd end up with:

@dec @num = 5;

I want to write:

@dec
private num = 5; // or private static num

Your description in this comment is how I would have imagined it to work.

zenparsing commented 8 years ago

I don't want to assume anything about decorators (their syntax or semantics) at this point...

Thanks for linking to my comment : ) I think it's a good documentation of why that solution won't fly.

glen-84 commented 8 years ago

I don't want to assume anything about decorators (their syntax or semantics) at this point...

Perhaps you're right, but they're already being used via Babel and TypeScript, so I'd be very surprised if the basic syntax changed.

Thanks for linking to my comment : ) I think it's a good documentation of why that solution won't fly.

Like I said, I can't really comment on the implementation, but I still don't believe that this is the only solution.

zenparsing commented 8 years ago

For the record, decorators are at stage 1, meaning that proposal is just as likely to change as this proposal.

erights commented 8 years ago

Agree with @zenparsing on almost everything. Disagree about # vs @. Despite the fact that both decorators and private fields are technically at stage 1, I really doubt that private fields will get @. Without it, I don't see plausible alternatives to #. Initially it hurt my eyes as well. However, after repeated exposure I rapidly became used to it. Now # simply means private when I see it followed immediately by an identifier.

Although we should strive for syntax which is initially more intuitive when possible, when it is not, don't underestimate how quickly novel syntax becomes natural on repeated exposure.

glen-84 commented 8 years ago

This is sad. Sure, we can get used to anything (I "got used" to writing PHP for like a decade, because I had to), but that doesn't mean that it's not ugly AF.

I sincerely hope that one or more of the implementers agree, and that a solution can be found that doesn't result in the syntax being butchered.

sarbbottam commented 8 years ago

Please excuse me for being late to the discussion. I wonder if we are discussing over the prefix, either # or @ at this point of time, whats wrong with anything else, to be precise private as prefix?

erights commented 8 years ago

Verbosity. foo.private x vs foo.#x. Also, the space within the first leads to the eye misparsing it. For example, foo.private x.bar does not suggest a parsing equivalent to (foo.private x).bar.

Btw, speaking of verbosity, I prefer foo#x to foo.#x.

sarbbottam commented 8 years ago

Can there be a block level variable withing the class { }, I'm sorry if this already have been discussed earlier.


class Stack {

  const stack = [];
  let top = -1;

  constructor() {}

  push(value ){
    stack.push(value);
    top += 1
  }

  pop() {
    stack.pop(value);
    top += 1
  }

  isEmpty() {
    return top === -1
  }

}
erights commented 8 years ago

Funny you should mention that. I like @sebmarkbage 's https://github.com/sebmarkbage/ecmascript-scoped-constructor-arguments proposal. However, since it leverages the intuitions around lexical variable capture, it should only be used to express instance-private instance state, not class-private instance state. Thus it would enhance both private fields and class properties, rather than replacing private fields. For both, it would solve the otherwise unpleasant issues #25 , https://github.com/jeffmo/es-class-fields-and-static-properties/issues/2 , and https://github.com/wycats/javascript-private-state/issues/11 , by allowing constructor-parameter-dependent initialization expressions.

Starting with https://github.com/sebmarkbage/ecmascript-scoped-constructor-arguments , it would seem to be a natural extension to also allow let and const variable declarations within the class body, as you suggest, to express further lexical-capture-like instance-private instance state. I do not yet have an opinion about whether this additional step would be worth doing.

yanickrochon commented 8 years ago

What about

class Stack {

  static const base = 100;
  static let increment = 0;

  const stack = [];
  let top = -1;

  constructor() {}

  push(value ){
    stack.push((base + value) * increment);
    top += 1
    increment += 1;
  }

  pop() {
    stack.pop(value);
    top -= 1
    increment += 1;
  }

  isEmpty() {
    return top === -1
  }

}
erights commented 8 years ago

What would it mean?

yanickrochon commented 8 years ago

Seriously? ...well, here's another example

class Thing {
  // static private field
  static let count = 0;

  static create(name) {
    return new Thing(String(name));
  }

  // private field bound to instance
  let originalName;

  constructor(name) {
    this.name = originalName = name;
    count += 1;
  }

  getName() {
    if (this.name !== originalName) {
     return count + '@' + this.name + ' (' + originalName + ')';
    } else {
     return count + '@' + this.name;
    }
  }
}

new Thing('foo').getName();
// -> "1@foo"
new Thing('bar').getName();
// -> "2@bar"
let t = Thing.create('test');
t.name = 'something';
t.getName();
// -> 3@something (test)"
sarbbottam commented 8 years ago

@yanickrochon two quick questions

yanickrochon commented 8 years ago

@sarbbottam I ran out of ideas :stuck_out_tongue: It's only meant to prove a point. The method is useless in itself, but only show usage in syntax. As for String(name)... I don't know!

erights commented 8 years ago

This use would be equivalent to just placing the let count = 0 declaration above the class, using real lexical capture rather than trying to emulate it by other means.

Nevertheless, if we allow instance let and const declarations as an extension to https://github.com/sebmarkbage/ecmascript-scoped-constructor-arguments , then perhaps least surprise would lead us to adopt the static forms as well. Interesting.

yanickrochon commented 8 years ago

@erights indeed, the static in context is the same, but is meant for consistency more than usefulness; a syntax sugar to wrap all functionality inside the class and avoid separation of responsibility.

sarbbottam commented 8 years ago

This use would be equivalent to just placing the let count = 0 declaration above the class,

In that case count will be shared across all the instances. Which is undesirable.


// src/stack.js
const stack = [];
let top = -1;

class Stack {
  constructor() {}
  push(value ){
    stack.push(value);
    top += 1
  }
  pop() {
    stack.pop(value);
    top += 1
  }
  isEmpty() {
    return top === -1
  }
  log() {
    console.log(stack);
  }
}

module.exports = Stack;
// test/stack.js
const Stack = require('../src/stack');

const stack1 = new Stack();
stack1.push(1);
stack1.push(2);
stack1.push(3);

const stack2 = new Stack();
stack2.push(11);
stack2.push(22);
stack2.push(33);

stack1.log();
// expected [ 1, 2, 3 ]; actual [ 1, 2, 3, 11, 22, 33 ]
stack2.log();
// expected [ 11, 22, 33 ]; actual [ 1, 2, 3, 11, 22, 33 ]
erights commented 8 years ago

If it is not shared across instances, what would you expect a class-level

static const stack = [];
static let top = -1;

to do?

sarbbottam commented 8 years ago

Ahh, I didn't mean the static ones, I guess I misinterpreted your comment.

On Mon, May 2, 2016, 2:29 PM Mark S. Miller notifications@github.com wrote:

If it is not shared across instances, what would you expect a class-level

static const stack = [];static let top = -1;

to do?

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/tc39/proposal-private-fields/issues/14#issuecomment-216370197

yanickrochon commented 8 years ago

@sarbbottam your last example would, thus, be equivalent to

// src/stack.js
class Stack {
  static const stack = [];
  static let top = -1;

  constructor() {}
  push(value ){
    stack.push(value);
    top += 1
  }
  pop() {
    stack.pop(value);
    top += 1
  }
  isEmpty() {
    return top === -1
  }
  log() {
    console.log(stack);
  }
}

module.exports = Stack;

In this case, top should simply be

class Stack {
  static const stack = [];
  let top = -1;
  ...

and make it an instance field.

(EDIT: well, in this example, both fields should definitely not be static!)

sarbbottam commented 8 years ago

well, in this example, both fields should definitely not be static!)

Yep!

My bad, I misinterpreted @erights comments, I overlooked that you (@yanickrochon) used static in the modified example. Sorry for the confusion.

This use would be equivalent to just placing the let count = 0 declaration above the class, using real lexical capture rather than trying to emulate it by other means.

amiller-gh commented 7 years ago

@zenparsing, I'm not entirely sure I see why we can't achieve what @yanickrochon proposed above with the private keyword.

Lets entertain the thought for just one more moment and play with this, can we? :)

Good 'ol C++ take an approach that many programming languages seem to take, (Java and Ruby do something pretty similar, no?). Contrived example:

#include <iostream>
using namespace std;

// Base class Person with a private `name` and `sayHello` property.
// Public class `talk` calls `sayHello` on the instance, and another passed Person.
// All methods defined in this class will *ALWAYS* use the private `this->name`, regardless of the instance's public properties.
class Person
{
public:
   Person(string n){
     this->name = "Person " + n;
   }
   void talk(Person a){ 
     this->sayHello();
     a.sayHello(); 
   }

private:
   void sayHello() const { cout << this->name << " Says Hello World\n"; }
   string name;

};

// Class Client extends Person and has a public `name` property.
class Client : public Person 
{
    public:
        Client(string n) : Person(n) {
          this->name = "Client " + n;
        }   
        string name;

};

// SecretClient once again makes `name` private.
class SecretClient : public Client
{
    public:
        SecretClient(string n) : Client(n) {
          this->name = "SecretClient " + n;
        }

    private:
        string name;
};

int main()
{
   Person person ("Adam");
   Client client ("Melissa");
   SecretClient secretClient ("Trevor");

   /* 
      Log: 
         Person Adam Say Hello World
         Person Melissa Says Hello World
   */
   // Note that it uses the private Person.name value in the Person.sayHello method.
   person.talk(client);

   /* 
      Log: 
         Person Melissa Say Hello World
         Person Trevor Says Hello World
   */
   // Note that it uses the private Person.name value in the Person.sayHello method.
   client.talk(secretClient);

   // Attempting to access Person.name results in an Error
   // cout << person.name << "\n";

   // Accessing Client.name returns the public Client.name value
   cout << client.name << "\n"; // Log: "Client Melissa"

   // Attempting to access SecretClient.name results in an Error
   // cout << secretClient.name << "\n";
}

C++ Classes seem to abide by these rules:

(I haven't worked heavily in C++ in a long time, does anyone know if there is a way to access the instance's value instead of the private value in a Class with a private value definition?)

I propose using something like C++'s lookup scheme, but with a couple tweaks: 1) Private properties defined in a class are not stored on the prototype chain, but in a separate "private slots" map. The class's prototype has undefined set as this key's value – enumerable, not-writable, and non-configurable. 2) As such, for all property accessors running outside of a class scope, accessing a private variable will just return undefined, as though the property is not present – which it isn't. The majority of lookups remain unchanged. 3) Because the prototype and property lookup remain unchanged, any classes extending this class with a public variable of the same name will see that public variable available to the public. 4) If the property access is happening inside of a class declaration, first check to see if this is a private variable name in this context. If so, use the private value, else fall back to prototype chain value – accessors in class declarations will always use the private slot value over a prototype chain value.

Like @yanickrochon, I unfortunately don't know much about implementation of the JS engines (working on it!), so I'm not sure how difficult determining "if the property access is happening inside of a class declaration" is, but barring a slow solution to that, I think we're looking at a fast private property lookup here because it is backed by a similar "private slot" method as the current proposal.

So this will give us:

Outstanding questions:

In JavaScript the above example would look like this:


// Class Person has a private property `name`. This private property will *always* be used for methods defined in this class, regardless of the instance's public variables.
class Person {
   constructor (n) {
     this.name = "Person " + n;
   }
   talk (a) { 
     this.sayHello();
     a.sayHello(); 
   }

   private var name;
   private sayHello() { 
      console.log(this.name + " Says Hello World\n"); 
   }

};

// The Client class extends Person and makes `name` public.
class Client extends Person {
   constructor (n) {
      super(); 
      this.name = "Client " + n;
    }   

    var name;
};

// SecretClient makes `name` private again, overriding anything set by Client.
class SecretClient extends Client {
   constructor (n) {
      super(); 
      this.name = "SecretClient " + n;
   }

    private var name;
};

   var person = Person("Adam");
   var client = Client("Melissa");
   var secretClient = SecretClient("Trevor");

   /* 
      Log: 
         Person Adam Say Hello World
         Person Melissa Says Hello World
   */
   // Note that it uses the private Person.name value in the Person.sayHello method.
   person.talk(client);

   /* 
     Log: 
         Person Melissa Say Hello World
         Person Trevor Says Hello World
  */
   // Note that it uses the private Person.name value in the Person.sayHello method.
   client.talk(secretClient);

   // Attempting to access private Person.name returns undefined
   console.log(person.name); // Log: "undefined"

   // Accessing public Client.name returns the public Client.name value
   console.log(client.name); // Log: "Client Melissa"

   // Attempting to access private SecretClient.name returns undefined
   // console.log(secretClient.name); // Log: "undefined"
}

To address some previous concerns above:

You'd have to somehow encode a "private key" into each lexical environment.

Uses same proposed private slot hash, just different differentiator for changing between private slots and prototype lookup.

You'd have to change the semantics of each and every property access (because any property access might result in a private field). Engines are highly optimized around the current property lookup semantics.

Current property lookup would remain largely the same. Only lookups inside of class definitions scopes would be different, and it should be (hopefully) no slower than the proposed private slot solution.

Would for-in enumerate over these properties?

Presumably not.

Can someone shadow a private property with a normal property lower on the prototype chain? What about the reverse?

Nope, uses same private slot solution with different lookup criteria.

How do you prevent leaking the names of private fields to clients that shouldn't know that information? This is probably a fatal information leak.

No leakage! External clients see the normal prototypical lookup like nothing is different

I probably just re-hashed something that you've already beaten to death in a less public venue, but I thought I'd take one more stab at making what sounds like the generally preferred syntax work. Eager to hear what you think!

Spiralis commented 7 years ago

I would love for the ES6 classes to just allow a variable block at the top, indicating scope access without it being available via this.. Which is similar to what javascript programmers are already used to. Actually, when I first tried ES6 and wanted to add "private" variables this is what I first tried, and was dissappointed to see that it didn't work.

It has already been proposed by @sarbbottam here and an example with static here. Unfortunately, after that the discussion went off trail in regards to static examples. But, those two examples are what I believe makes perfect sense for javascript developers.

How this can be solved implementation wise I don't know. Whether they will indeed be scoped variables or if one can utilize fast slot lookup, as referred to earlier.

littledan commented 7 years ago

@Spiralis That would form instance private (state only accessible within a particular instance) rather than class private (you can see the private state of things passed into you, and also of lexically enclosing classes). Instance private is more restrictive, and class private has some use cases, so this proposal goes with class private.

yanickrochon commented 7 years ago

I currently simulate private fields with Symbols and it works great. It's even possible to use a form a reflexion (see Object.getOwnPropertySymbols()) to get direct access, just like most languages allow :

const $value = Symbol('value');   // private Symbol

class Calculator {
  constructor(initialValue) {
    this[$value] = initialValue || 0;
  }

  add(modifier) { this[$value] = this[$value] + (modifier || 0); }
  sub(modifier) { this[$value] = this[$value] - (modifier || 0); }
  mul(modifier) { this[$value] = this[$value] * (modifier || 0); }
  div(modifier) { this[$value] = this[$value] / (modifier || 0); }

  get value() { return this[$value]; }
}

For example, the code above could be written as

class Calculator {
  private val;

  constructor(initialValue) {
    this.val = initialValue || 0;
  }

  add(modifier) { this.val = this.val + (modifier || 0); }
  sub(modifier) { this.val = this.val - (modifier || 0); }
  mul(modifier) { this.val = this.val * (modifier || 0); }
  div(modifier) { this.val = this.val / (modifier || 0); }

  get value() { return this.val; }
}

and be interpreted as the prior example, using auto-generated private Symbols in the same manner. To me, this seems to be a preferable solution as it uses JavaScript's already implemented features without creating a new, clumsy, construct or adding a new notation.

dfahlander commented 7 years ago

Seems the following discussion fits in here as well: https://github.com/jeffmo/es-class-fields-and-static-properties/issues/37

amiller-gh commented 7 years ago

Since there is still discussion here, can we re-open this issue @zenparsing?

@dfahlander, not sure if I like #37 in principle, but its also irrelevant to this proposal because it would only supplement behavior defined here regardless of the chosen syntax, no?

ljharb commented 7 years ago

@yanickrochon fwiw, in no way is that private - Symbols are never private, and can always be retrieved via reflection, for example, Object.getOwnPropertySymbols. Your approach certainly will avoid accidental name collisions - but they're all still 100% public properties.

yanickrochon commented 7 years ago

@ljharb as I said. And also as I said, most languages do allow accessing private fields and properties through reflexion or some other form. Dammit, even JavaScript already has basic support for reflection via the Reflect class!

It is misleading (and certainly a bad concept) to allow a true blackbox in regards of private fields. I cannot fathom why nor see any real reason why this would be desirable in JavaScript, as this is why the language is not only widespread, but beautiful; everything is accessible. From the module loader to the bare prototype of any object; things are documented and accessible in one way of another. Doing anything else would simply be an aberration.

The idea behind private is not (or should not) to create a blackbox, but to define a semantic in the language. Something that not only blends with the current syntax, but that is also familiar in the C-style syntax. Something better than the actual _ commonly used prefix! Using Symbols is nothing but logical, as it simply cannot be done without scanning for the object's own Symbols (i.e. which is consistent with the private nature, as you cannot directly access private fields of the super class either this way). So, you see, they are definitely not "100% public properties". Not any more or any less than private attributes of a Java class.

ljharb commented 7 years ago

I don't think Java's model of "encapsulation" is one we want to emulate.

True encapsulation - even from reflection methods - is critical to restrict your exposed API surface.

amiller-gh commented 7 years ago

I'm torn by "true encapsulation". The ability to use reflection on "private" variables would certainly be nice for testing and debugging, and I rather like the mental model of private as syntactic sugar for symbol properties. It also means that private properties could be easily transpiled using babel – there seem to be more than a few plugins out there that already do that. If the only tradeoff for ease-of-testing, consistency with existing JavaScript concepts, and ability to use today in the browser is exposure to reflection and proxies, than following in the footsteps of C# and Java may not be the worst idea.

However, I also agree that "true encapsulation" is, holistically, preferable and not possible using any variation on Symbols: https://github.com/tc39/proposal-private-fields/issues/13 ¯(ツ)

But, all this has nothing to do with the syntax. Perhaps its worth taking it to another conversation thread? @ljharb, any opinions on using the private keyword instead of the # prefix as described in my (exceptionally long) comment above?

zenparsing commented 7 years ago

It is misleading (and certainly a bad concept) to allow a true blackbox in regards of private fields.

From the module loader to the bare prototype of any object; things are documented and accessible in one way of another. Doing anything else would simply be an aberration.

Not so. True encapsulation is already present in Javascript in the form of closure variables as well as objects created from spec-defined constructors (e.g. Map, Promise, etc.).

Spiralis commented 7 years ago

I personally certainly prefer using private myVariable over #myVariable. And, to be honest, if anyone breaks my API by reflecting out the private parts, for me that is acceptable. There are probably use-cases out there where true encapsulation would be important too. But, are there enough of them to warrant this to be a necessity for Javascript? Especially when the trade-off is to use a prefix/keyword that is so counter-intuitive, and by many even considered so "ugly", that it might actually put people off? I think not.

@ljharb:

True encapsulation - even from reflection methods - is critical to restrict your exposed API surface.

I'd agree with this statement - as it stands. But for this to be valid we need to turn it around: For which scenarios is restricting the exposed API surface critical? My answer to this question is: I have never experienced a project where this was a requirement.

My vote goes to @yanickrochon on using private (perhaps with symbols under the hood to achieve this).

This is my reasoning for getting to this conclusion:

Private vs #

True encapsulation:

So, for me this is a clear win to private without true encapsulation.

zenparsing commented 7 years ago

I get that people want to write private and be done with it. I've written over and over again why that doesn't work and I'm sorry that I haven't been able to communicate the idea sufficiently.

@littledan This is the negativity toward # that I was talking about. It is helping to fuel a general backlash against the proposal's semantics.

yanickrochon commented 7 years ago

@zenparsing Closure variables are not private fields. Private fields are simply properties that API designers set, specifying that they are not to be tempered with. I am fine with using something like

let foo = (function () {
  const trueEncapsulatedVar = 'Hello';

  return new class Foo {
    get bar() { return trueEncapsulatedVar; }
  };
})();

if it is really required. However, and as @Spiralis wrote, there are so few cases where this would really be required that it outweighs the argument. Most API would really benefit from accessible private properties (i.e. through reflexion) for testing purposes. Removing that feature will only lead to bad API design and abuse and obscure implementations.

yanickrochon commented 7 years ago

For the heck of it, this can already be achieve with the current JavaScript implementation :

const $privStatic = Symbol('privStatic');
let privStatic = 'static private';

let closureStatic = 'static closure';

const Foo = (() => {
  const $priv = Symbol('priv');
  let priv = 'private';

  let closure = 'closure';

  class Foo {
    static get clos() { return closureStatic; }
    static set clos(v) { closureStatic = v; }

    static get priv() { return Foo[$privStatic]; }
    static set priv(v) { Foo[$privStatic] = v; }

    constructor() {
      this[$priv] = priv;
    }

    get clos() { return closure; }
    set clos(v) { closure = v; }

    get priv() { return this[$priv]; }
    set priv(v) { this[$priv] = v; }
  }

  Foo[$privStatic] = privStatic;

  return Foo;
})();

let foo = new Foo();

console.log("Static closure:", Foo.clos);
console.log("Static private:", Foo.priv);
console.log("Closure:", foo.clos);
console.log("Private:", foo.priv);

So, my point is that most people don't agree or even care for a special case of private fields (i.e. adding # as prefix) and the consensus seems leaning toward semantics. Where this awkward piece of code could be written as :

class Foo {
  static private privStatic = 'static private';
  static private let closureStatic = 'static closure';

  static get clos() { return closureStatic; }
  static set clos(v) { closureStatic = v; }

  static get priv() { return Foo.privStatic; }
  static set priv(v) { Foo.privStatic = v; }

  private priv = 'private';
  private let closure = 'closure';

  get clos() { return closure; }
  set clos(v) { closure = v; }

  get priv() { return this.priv; }
  set priv(v) { this.priv = v; }
}

let foo = new Foo();

console.log("Static closure:", Foo.clos);
console.log("Static private:", Foo.priv);
console.log("Closure:", foo.clos);
console.log("Private:", foo.priv);

through Symbols, attaching static ones on the prototype and instances ones on this. What do you want more?

ljharb commented 7 years ago

All things being equal, the difference between private priv = 'blah' and #priv = 'blah' is pretty minimal - and if there are reasons why private won't work, then it's kind of a moot point.

@yanickrochon's example is not equivalent, because Symbols are not private. Accessible === public, and all Symbols are always accessible.

fwiw, true encapsulation imo is always required, because whenever it's not present, there are inevitable bugs around access to those things. Changing something private while retaining the same semantics in the public API should never break anything - if it is possible, then it's not actually private.

The analog to "private fields" here is "internal slots" in the ECMAScript spec. These are not properties, they are utterly private bits of information stored alongside various JS values. Adding private fields simply provides a way for users to set custom internal slots - things that are truly inaccessible, in all cases, without the proper privilege/access/scope.

amiller-gh commented 7 years ago

I'm not convinced that providing access to custom internal slots is predicated by the using the # prefix. It is possible to have true encapsulation and use the private keyword. A slew of other languages accomplish it and I'm not sure why JavaScript can't too. Treating private the same way as other C based languages will only help developers coming to JavaScript from other walks of life pick up the language, and improve code legibility across the board. I'd be happy to take a stab at writing a C++ style private value lookup version of the proposal to tear apart together if there is interest.

ljharb commented 7 years ago

@epicmiller private might work for declaring properties, but how would it work for accessing them? There should be a difference between "this[private foo]" and "this.foo", and the example i just gave doesn't seem ideal.

sarbbottam commented 7 years ago

but how would it work for accessing them?

Should't a private be accessible via public instance interfaces, similar to how its works in Java and other languages.

There should be a difference between "this[private foo]" and "this.foo"

Not sure if I follow this, shouldn't it be always this.foo? And whether this.foo is accessible depends on the current scope and context?

zenparsing commented 7 years ago

@epicmiller Javascript doesn't have a static type system, and so for any variable "v", can't distinguish whether "v.x" should reference the private "x" or the normal property keyed "x".

Yes, you can probably devise some trickery to make it work in a way that seems nice to you, but:

Based on those considerations, a TypeScript-like private declaration in dead-on-arrival.