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

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

Closed
melanke opened this issue Nov 2, 2015 · 755 comments
Closed

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

melanke opened this issue Nov 2, 2015 · 755 comments

Comments

@melanke
Copy link

melanke commented Nov 2, 2015

No description provided.

@melanke melanke changed the title I woudl hate to use '@' instead of 'private', and why we need '@' all the time I woudl hate to use '@' instead of 'private', and why we need '@' all the time? Only in declaration of the variable would be enough, like Java or C# Nov 2, 2015
@melanke melanke changed the title I woudl hate to use '@' instead of 'private', and why we need '@' all the time? Only in declaration of the variable would be enough, like Java or C# I would hate to use '@' instead of 'private', and why we need '@' all the time? Only in declaration of the variable would be enough, like Java or C# Nov 2, 2015
@zenparsing
Copy link
Member

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:

  • Look up the "a" named property on otherX, or
  • Look up the private field named "a" on otherX.

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:

  • We could use a different lookup operator: @, for instance. this@x = 123
  • We could use a different kind of property name: @property, for instance. this.@x = 123.

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.

@zenparsing zenparsing changed the title I would hate to use '@' instead of 'private', and why we need '@' all the time? Only in declaration of the variable would be enough, like Java or C# Why not use the "private" keyword, like Java or C#? Nov 2, 2015
@yanickrochon
Copy link

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

@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'd have to somehow encode a "private key" into each lexical environment.
  • 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.
  • Would for-in enumerate over these properties?
  • Can someone shadow a private property with a normal property lower on the prototype chain? What about the reverse?
  • 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.

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 zenparsing/es-abstract-refs#11 for more information on why we don't want to use a "private" version of Symbols here.

@yanickrochon
Copy link

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

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

melanke commented Jan 22, 2016

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

glen-84 commented May 1, 2016

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

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

glen-84 commented May 1, 2016

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

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

glen-84 commented May 1, 2016

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

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

@erights
Copy link

erights commented May 2, 2016

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

glen-84 commented May 2, 2016

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

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

erights commented May 2, 2016

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

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

erights commented May 2, 2016

Funny you should mention that. I like @sebmarkbage 's https:/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 , tc39/proposal-class-public-fields#2 , and wycats/javascript-private-state#11 , by allowing constructor-parameter-dependent initialization expressions.

Starting with https:/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
Copy link

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

erights commented May 2, 2016

What would it mean?

@yanickrochon
Copy link

yanickrochon commented May 2, 2016

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

@yanickrochon two quick questions

  • Why do you need static create(name)? One should be able to new Thing(name)
  • Why String(name) and not new Thing(name)?

@yanickrochon
Copy link

yanickrochon commented May 2, 2016

@sarbbottam I ran out of ideas 😛 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
Copy link

erights commented May 2, 2016

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:/sebmarkbage/ecmascript-scoped-constructor-arguments , then perhaps least surprise would lead us to adopt the static forms as well. Interesting.

@yanickrochon
Copy link

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

@RyanCavanaugh
Copy link

And other questions, why not just pick the most popular answer for each question?

I cannot think of a worse way to design a language.

@DaSchTour
Copy link

I just wonder why do we want two properties with the same name, one public and one private. Just disallow two properties with same name in one class and bury this ugly # syntax.

@ljharb
Copy link
Member

ljharb commented Jun 27, 2019

@DaSchTour please see the FAQ where that's addressed.

@MarcoMedrano
Copy link

this.#proposal sucks!!!
TypeScript does not have these problems and has many other features besides encapsulation years ago.
I know you want to make it kind of old Js compliant and back compatible, but that way no sense on making class strict by default.

@ljharb
Copy link
Member

ljharb commented Dec 17, 2019

@MarcoMedrano TypeScript definitely has these problems, and its "private" is fully public at runtime - proper privacy, I'm told, is one of their most requested features. Please see the FAQ for why "hard private" is the choice that's been made.

@charlesr1971
Copy link

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

@zenparsing Well, I must say that @ is marginally better than #. And, I am afraid this thread goes way above my pay grade, so I won’t even bother trying to persuade you to use private, but please please please, pretty please, try and make private work :)

@charlesr1971
Copy link

charlesr1971 commented Jan 31, 2020

@zenparsing Just out of interest could we use var to prefix private variables like:

class Foo {

  var x = 'foo';

  constructor() { }

  set _y(y) {
  	this.y = y;
  }

  get _y() {
  	return this.y;
  }
  
  set _x(x) {
  	x = x;
  }
  
  get _x() {
  	return x;
  }

}

let x = new Foo();
x._y = 'bar';
x.y = 'foo'; 
x._x = 'bar';
x.x = 'foo'; // throws exception: cannot access private field
console.log(x._y); // foo
console.log(x._x); // bar

@RyanCavanaugh
Copy link

Private fields are operational in the very browser you're using. The ship has sailed and suggesting new syntax at this point is pointless except as a thought exercise.

@rdking
Copy link

rdking commented Feb 1, 2020

That ship has not reached international waters yet (reached stage 4). It can still be recalled, though it's unlikely. Speaking of which, why? If TC39 is in such a hurry to get this ... proposal, warts and all, into the language, why hasn't it reached stage 4?

@ljharb
Copy link
Member

ljharb commented Feb 1, 2020

Nobody’s “in a hurry”; this proposal has taken many, many years. Stage 4 requires at least two shipping web browsers, and i suspect that this proposal’s champions will want as much experience as possible before advancing.

Separately, things can be “recalled” after stage 4, and can be “un-recallable” before - web compatibility is what’s most important here, which is why stage 3 is the time for implementations. If there’s sites that depend on a feature, browsers are unlikely to break them, and that includes this one.

@1valdis
Copy link

1valdis commented Feb 10, 2020

The day this # syntax lands in specification will be a sad day for Javascript.

@Jordan-Hall
Copy link

@1valdis ECMAScript isn't just JavaScript though. TBH I've been using the new hash now and tbh it's relatively clean and easy to type. I've changed my mind and the sigil now, it's less intrusive than using private keyword. Plus less rewrite of the meaning of "this"

@rdking
Copy link

rdking commented Feb 10, 2020

Though somewhat disagreeable, either that sigil or something else would need to be used even if "private" were the declarative keyword. That is a fixed issue. That TC39 has implicated '.' for "private" declarations is another. Using the # was never the biggest of the syntax-related issues. And the syntax issues were never the biggest issues with the proposal. All in all, use of the # is only a small, distasteful issue.

BTW, the "intrusiveness" of the "private" keyword is intentional. It makes it clear at a glance that something different is going on with this key, as opposed to # where the details of the syntax must be learned to understand that it is an access modifier (that tries to pretend it's a name character).

@CavidM
Copy link

CavidM commented May 12, 2020

Yes, its a default behaviour of languages. For analyzing you can see the same behivour in other languages. Example in Java and Php.

@trusktr
Copy link

trusktr commented May 13, 2020

@CavidM In those examples, there is no absolute hard privacy.

In Java, having a private property X means that you can not have a public property X that is accessible in public scope, which means in certain cases the existence of the private property can be detected (putting aside Java reflection which allows accessing private properties).

In the PHP example, it is trivial to detect the private field because trying to access it from the public side leads to a detectable runtime error.

So in Java and PHP, public code can detect the presence of, or even use, private properties.

But TC39 wanted absolute hard privacy, meaning that at runtime it should be 100% impossible to rely on being able to detect the private properties of an object, and definitely impossible to read them.

How this proposal achieves true hard privacy, is that running something like a.#foo outside of a class definition is simply a SyntaxError and your code won't even run.

This makes it impossible to detect private variables like you can in PHP by for example detecting an error by trying a property access and catching the error (and perhaps reading the message for information).

Try running this in your console:

 class Foo { #foo = 123 }
 const f = new Foo
 f.#foo

In this new proposal, the existence of #x does not prevent a user on the public side from creating a public property x. If it did, then the user would be able to determine (at runtime) that the private property #x exists.

That's why, we can't simply allow this.x inside the swap() method. If we did, how would we know if we want to access a public x, or a private x. We can't.

You'd need to propose something that adheres to the constraints if absolute hard privacy, but this.x currently does not because it is ambiguous.

@ghost
Copy link

ghost commented Aug 8, 2021

Although I don't like # it strikes me as better than @ because it refers to "hashing" instead of "injection".

@raphaelvdossantos
Copy link

End of story:
We hear you but screw this we gonna do it this way anyway because we want it.
period.

@ghost
Copy link

ghost commented Sep 6, 2021

To be honest, I would have preferred the option of both hard and soft privacy. Hard privacy could have used the const/let syntax without indexing this (think block scope for entire class!) while soft privacy would have used the private keyword

The ship has sailed, the cat is out of the bag, the bed is made, etc. What's done is done and I guess TS will always have the private keyword

In any case I am sorry for the notif on this as well, it's a little annoying to get notifs on this dead horse

@charlesr1971
Copy link

I like your thinking, except I would use var, instead of const, let

@ghost
Copy link

ghost commented Sep 6, 2021

Any would work - the real feature would be making the class a block scope for variables or scripting logic

@ljharb
Copy link
Member

ljharb commented Sep 6, 2021

@sndst00m that already exists, and just like this proposal, is stage 4. https:/tc39/proposal-class-static-block

@rdking
Copy link

rdking commented Sep 7, 2021

@ljharb If memory serves me, the point of the const/let notation was that it created a block scope that would be carried along by instances and applied as a scope layer to each function defined in that scope. Making a static scope block is a completely different thing.

@ghost
Copy link

ghost commented Sep 7, 2021

Essentially the braces of class { ... } would work just like the braces of function Fn() { ... }

@ljharb
Copy link
Member

ljharb commented Sep 7, 2021

Ah, I misunderstood. I'm pretty confident that kind of design has been explicitly rejected on multiple occasions, but I don't have notes off hand.

@rdking
Copy link

rdking commented Sep 7, 2021

That's a real shame considering that design not only would likely have been the easiest to implement, but also the easiest to understand given that it would have just been binding a closure to an instance and giving developers something they already conceptually understood as the implementation of private.

@ghost
Copy link

ghost commented Sep 7, 2021

Can functions called with bind/call access those private variables however?
Not that using .bind with private properties should be possible really..

@rdking
Copy link

rdking commented Sep 8, 2021

No function that did not get defined within the scope of the class definition will ever have access to private members. Trying to late-bind them in will not work.

@hantsy
Copy link

hantsy commented Nov 7, 2021

In my opinion, It is terrible that a private field is marked with a # prefixes. A private modifier is 100 times clearer than #.

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