-
Notifications
You must be signed in to change notification settings - Fork 37
Syntax change suggestion #24
Comments
@littledan Let's see how my suggestion compares with your FAQ.
The example and its expansion above should be in keeping with all the other points of your FAQ,, not withstanding the above comments. Put another way, you'd get the features you want without burdening us with ugly, somewhat confusing syntax, without changing the existing fact that |
I don't understand what you mean by this. They would be able to use it, and it would do the wrong thing. The example I have in mind is this: class A {
private x;
constructor(foo) {
this.x = foo;
}
} That creates a public field, rather than setting the private one. |
@bakkot Absolutely correct. Under the syntax I'm proposing, if you wanted to set a value on the private field x in the constructor, it'd look like this:
While I don't completely agree that the objectives of this private methods proposal are the right thing to do, this time I decided to ignore my own objections and work within the intents of the proposal to at least propose a less abrasive syntax. To that end.... Given that |
I think that a lot of people would try to use the syntax in my example to set a private field, and might not even notice that they've actually created a public field. I think that is a bad thing. |
I think you're right that lots of people will trip over that one, at least at first. Whether it's the use of a sigil or this more palatable syntax, there's a learning curve. However, I think the learning curve for the syntax I'm suggesting to be significantly smaller and easier to digest than the sigil syntax since it's very much already a risk. The syntax already exists to make that kind of mistake: var foo = Symbol();
var a = {};
a[foo] = 1;
console.log(a[foo]); // 1
console.log(a['foo']); // undefined, muscle-memory typo While this may be seen as a bad thing by some, it really doesn't change anything. The truth of the matter is that such mistakes can usually easily be caught by unit testing, and the code will usually either work properly any way or cause a surprise error or misbehavior that will lead directly back to the improperly used syntax. That only leads to programmers learning how to use the syntax faster. That's a good thing in my book. |
@rdking Do you have a technical argument against the
Sure, many people have come to this repo and said they disliked the syntax, but that's not exactly an empirical survey. And while people might think it looks strange at first, I think they will grow accustomed to it especially when they learn that there are technical reasons for favoring it over a Anyway, I don't think that personal distaste for the syntax is a very strong argument at this point after all the discussion on this. |
I just made a post here describing more precisely my proposed changes to the syntax, implementation details, and how |
Warning: I was asked for a technical argument! This a TL;DR post! @mbrowne I actually explained my POV on that above where I addressed the FAQ. If you want more of a technical justification, I can give you that too. When comparing the use of If you're wondering why that's relevant, it's because For interpreted languages, like ES, there's not really a compile phase. So access determination and code can't easily be created before the code is run. In the case of self-modifying dynamic languages, the situation is worse. Since new code can be created on the fly, it is absolutely impossible to determine access and generate appropriate code before runtime. Hence, for interpreted languages, This is the primary reason why there is and should be a difference in "meaning" between Now for the fun part... The first technical reason not to use the sigil, and to use The second reason is about the proposed use of sigil notation. In the following declaration: //Sigil version
class Test {
#member;
}
//My version
class Test {
private member;
} As compared with the sigil version, where developers would have to come to grips with:
Since (3) has 2 parts to it, it's effectively 4 things that developers "would have to get used to". In comparison, in my notation: The fact that access to "member" must be done using There's a 3rd reason as well, and it goes back to @bakkot comment:
He was referring to the potential to conflate Since sigil notation requires access to be |
I'm trying to determine if the current proposal adds private methods to the prototype, which is something that would be impossible in existing JS...glancing at the proposal I'm not sure and don't have time to fully read it right now. But if it does then that is a difference that should be considered between the current proposal and your proposal (among many other considerations of course). |
While ugly, the # is easier to explain to new programmers:
vs
On the former the new programmer would just accept the "magic", on the later you would have to explain why. |
@borela I think you might be making a bad assumption there, namely that The bit about it being a "class-private name" vs a "private instance member" is critically important. ES already has a construct very similar to class-private names: |
Well, BTW here's the link to the FAQ @rdking mentioned: https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md |
No, I meant that for a new programmer, it would work like "magic", you would not need to explain how it is implemented just that he has to use the #. |
Ah. I guess I misunderstood. :) In truth, however, you would still need to explain for the sigil notation since it wouldn't outherwise make sense that neither |
Agree, not being able to do |
The only problem is that is in fact necessary that with sigil notation |
I'm not sure if this is well-documented, but a key goal of the current proposal is to make private names not be property keys and not use square bracket-based access, in order to make sure that Proxies cannot observe reads and writes to private fields. There's another goal that all square bracket access go through Proxy traps. This proposal gets around the whole thing by making private field access a different syntactic production. |
@littledan I've been waiting to get your opinion. Is "not use square bracket-based access" actually a goal by itself? Or is it just something seen as necessary due to the peculiarities of the sigil notation? In either case, the following goals you've listed are preserved under my suggestion:
Barring your goal? of not using square-bracket notation, the suggestion I've provided satisfies all of them, and without:
By the way, I've got a potential 4th technical reason that has to do with (3) above. From the ES specification, section 6.1.7.2, paragraph 3. About half-way through, you'll see this:
Your current description of implementation for private names would cause you to need to either:
If your implementation chooses the 1st approach, there's no issue save for the fact that now I think I've either shown, or can show that:
What I would like to know is if there is anything I've missed that still gives sigil notation an advantage over my suggestion that's not paired with an additional complication. |
A lot of good points here, just wondering about this:
How so? |
@littledan There's one other thing I wanted to know about your private name related proposals. Below I've written a /* With Private Sugar */
var publicSymbol = Symbol();
class Example {
private member = 0;
/* public */ actions = 0;
private method() {
console.log(`member = ${this[member]}`);
}
constructor(val) {
if (!NaN(val))
this[member] = val;
this[publicSymbol] = "Pointless string!";
}
action() {
++this.actions;
this[member] += actions;
this[member] **= 2;
}
action2() {
console.log(`publicSymbol = ${this[publicSymbol]}`);
}
}
/* Without Private Sugar */
var publicSymbol = Symbol();
var Example = (function describeExample() {
var privateScopes = new WeakMap();
var privateNames = {
member: Symbol(),
method: Symbol()
};
with(privateNames) {
var privateMethods = {
[method]: (function method() {
console.log(`member = ${privateScopes.get(this)[member]}`);
})
};
return class _Example {
constructor(val) {
privateScopes.set(this, {
[member]: 0,
[method]: privateMethods[method].bind(this)
});
this.actions = 0;
if (!NaN(val))
privateScopes.get(this)[member] = val;
}
action() {
++this.actions;
privateScopes.get(this)[member] += this.actions;
privateScopes.get(this)[member] **= 2;
privateScopes.get(this)[method]();
}
action2() {
console.log(`publicSymbol = ${this[publicSymbol]}`);
}
}
}
})(); I hope this example and its de-sugared version serve to answer any questions you might have about my suggested syntax and implementation, and help you poke holes if there are any. |
@mbrowne I explained some portion of this in a different thread before. This time I'm going to do like I did for the example code above. /* With Private Sugar */
class Base {
private member = 0;
protected member2 = "Yay! Protected members!";
}
class Derived extends Base {
constructor() {
super();
}
}
/* Without Private Sugar */
//Defined by the language, maybe not visible to developers.
Object.defineProperty(Object.prototype, "protectedNames", {
value: Symbol()
});
/**
* @param {object?} ipn - protected names as defined by the base class
*/
var Base = (function describeBase(ipn) {
ipn = ipn || null;
var privateScopes = new WeakMap();
var privateNames = {
member: Symbol(),
member2: Symbol(),
__proto__: ipn
};
var protectedNames = { __proto__: ipn };
Object.defineProperty(protectedNames, "member2", {
value: privateNames.member2
});
with(privateNames) {
class _Base {
constructor() {
privateScopes.set(this, {
[member]: 0,
[member2]: "Yay! Protected members!"
});
}
}
Object.defineProperty(_Base, Object.protectedNames, {
value: protectedNames
});
return _Base;
}
})();
var Derived = (function describeDerived(ipn) {
ipn = ipn || null;
var privateScopes = new WeakMap();
var privateNames = {
__proto__: ipn
};
var protectedNames = { __proto__: ipn };
with(privateNames) {
return class _Derived {
constructor() {
super();
}
}
Object.defineProperty(_Derived, Object.protectedNames, {
value: protectedNames
});
return _Derived;
}
})(Base[Object.protectedNames]); The only problem with this approach is that the protected names are publicly available on the |
It's not a goal by itself, but @erights @tvcutsem and others have designed Proxies to meet particular goals about the way the object model works. Desugaring |
Most of the de-sugaring would happen at parse time, barring the usual hiccups like use of Put another way, I'm expecting the engine to hide the fact that all private fields are actually not part of the instance used to access them. In doing so, this actually neither adds nor removes anything from the object model. So there is no noticeable effect on Proxy. I've recently offered a totally different idea for changing how Proxy works. You can browse the es-discuss mailing list to see it. |
@rdking Distinguishing ordinary property access from private member and adding the correct scopes would require some sort of intellisense which is slow, specially when dealing with large files/projects. |
I don't understand what the advantage of this proposal would be. It seems confusing to have |
Even if the evaluation is at runtime, the effect on Proxy is the same. Since evaluations that result in the use of a private name will not occur against Where you say the use of
Put bluntly, I'm not changing anything that an ES developer currently expects with my syntax. I'm only adding something new, a meaning for the already reserved
developers will know everything they need to know about adding private members. The number of things you need to know, including the above statement but with In the end, the use of sigil notation introduces more problems than avoiding |
I think it's more confusing to have two completely different meanings for |
@littledan That is disappointing.
If your reason for refusing my suggestion is as you've stated, then I submit you're already trying to do the same, but with a completely new token. I have to ask (for my own edification), is it worth:
just to avoid what you think is higher "mental overhead" for people who are arguably already accustom to both the |
To @littledan's point, I do think it would be a little confusing to people that public and private properties would work so differently with respect to
@rdking, I do think you've made a lot of good points (and some of the counter-arguments like @borela's comment about syntax highlighting I don't find very convincing). To be fair, there are plenty of non-intuitive things about the sigil notation as well, as you have outlined. But on the whole it seems like most of the potential confusion from sigil notation is simply a matter of it looking strange and unfamiliar, which of course is only a very temporary problem. I agree it would be very nice if the
I'm guessing that in number 2 in the above list, you're referring to the fact that you can do this:
In any case, when looking at the examples in the readme for both proposals, and reading the private fields FAQ, it all looks pretty clear and unambiguous. |
@mbrowne As a matter of clarification: This is both a logical and technical reason for the choice I made. Isn't claiming it to be non-intuitive (to which I will admit it isn't directly intuitive), and using that to deny due consideration for the advantages of my approach over the proposed, no different than all the attempts made to get @littledan to change the syntax over it's non-intuitive properties? If so, then the same response applies: "they'll get used to it". I understand why some may think it would be confusing, but after over 30 years of programming in even more languages, and even writing a few of my own, I have a good eye for what needs to go on under the hood to make things work. Whether it's the sigil syntax or what I've offered, there's a learning curve. Even if everyone thinks the learning curve for my approach is higher, I still think that what we'd all get in return would be better in both the short and long run with respect to the stability, usability, and extensibility of ES. I understand that years of discussion has already gone into this, but even if in the form of links to responses already given, I'd like to see some logical, technical discussion over the issues I've raised that leans in favor of using the sigil. I don't particularly care if my suggestion is adopted. I would just hate to see the implementation of As for your example, let's ignore implementation details for just a moment and look at it with a slight modification: class Demo {
foo;
bar = Symbol();
method() {
this.foo; //works
this['foo']; //works
this['bar']; //works: accesses this.bar, a public
this[bar]; //works: accesses this[bar] where bar is a private symbol
const propertyName = 'foo';
this[propertyName]; //works: accesses this.foo
}
} Using this variation of your example, how confusing is Maybe I've been too transparent with my suggested implementation details. But that in itself is my point. It's an implementation detail. No developer will ever need to know this part. All the programmer needs to know to use the syntax is that:
Except for the part about the declaration being a As for your other comments, you guessed correctly. Also, I never claimed that the sigil syntax is anywhere unclear or ambiguous. I argued that it is a hard departure from current syntactic expectations in several ways. I argued that the implementation details required to make the sigil syntax work as described would add unnecessary changes and complications to both the ES specification and engines. I argued that the proposed use of internal slots would elevate What would settle me would probably be to see the proposed rewrite of ES spec section 9.1. My fear can be summarized like this: If this private names and methods proposals are implemented as currently prescribed, |
@rdking, the spec text for public and private class fields is already written. It makes no changes to section 9.1 in the spec, does not make You say above that there's no desugaring for the current proposal, but it can in fact be faithfully desugared to ES2015: let x = class {
#field = 0;
m(){
this.#field += 1;
return this.#field;
}
}; becomes let x = (()=>{
const fieldMap = new WeakMap;
const getFieldOrThrow = o => {
if (!fieldMap.has(o)) throw new TypeError;
return fieldMap.get(o);
};
const setFieldOrThrow = (o, v) => {
if (!fieldMap.has(o)) throw new TypeError;
fieldMap.set(o, v);
return v;
};
return class {
constructor(){
fieldMap.set(this, 0);
}
m(){
setFieldOrThrow(this, getFieldOrThrow(this) + 1);
return getFieldOrThrow(this);
}
};
})(); (modulo some Function.prototype.toString changes and assuming no changes to WeakMap.prototype, of course). It's slightly more complicated for derived classes, but not hugely. Nor is the implementation in engines necessarily particularly complex, since engines already must already have a concept of internal slots and since private field access does not share syntax with public field access. (My initial implementation in V8 was on the order of hundreds of lines.) By contrast, your proposal requires changing the semantics of every computed property access and exposing a new kind of object (namely, private symbols) to user code. |
I understood the changes my suggestion would require regarding computed property accesses. However, it wouldn't have required a new kind of object. I very literally wanted to use |
If the |
@mbrowne VSCode and Atom are already slow even on a high end computer, to highlight this properly, it would require parsing and knowing which entities were declared private, which is impossible with a simple regex highlighter. |
@bakkot The theory is that with |
I made the comments below in a different thread, but I think it warrants another look into the syntax all its own.
@littledan I get where you've been trying to go with the class-private names approach, though as someone who has written new languages, still think you've not thought thing through thoroughly (pardon the alliteration). An equals method can be developed even using private fields as described by @zocky while still taking into account most of what you're trying to achieve with private fields, and all without destroying the fact that
class
is just syntactic sugar. What if this example:translated to this:
I've spent much time trying to think of how the sigil (
#
) approach would work if implemented using current JavaScript. Frankly, it would probably look like the above. What's more, is that unless I missed something critical, what you're trying to do with class-private names can be in a fairly straight-forward manner be little more than a hidden implementation detail while allowing the syntax to remain something more palatable to the existing Javascript developer base.The text was updated successfully, but these errors were encountered: