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

Syntax change suggestion #24

Closed
rdking opened this issue Jan 10, 2018 · 35 comments
Closed

Syntax change suggestion #24

rdking opened this issue Jan 10, 2018 · 35 comments

Comments

@rdking
Copy link

rdking commented Jan 10, 2018

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:

class myClass {
  private foo = "bar";
  doSomethingWithFoo() {
     // foo is accessible here, but not accessible to methods of this or any other object
     // defined outside the class definition 
     doSomething(this[foo]);
  }
  equals(other) {
    let retval = false;
    if (other instance of myClass)
      retval = this[foo] === other[foo];
    return retval;
  }
} 

translated to this:

var myClass = (function defineMyClass() {
  var privateScope = new WeakMap(); //Your private slots go here
  var privateNames = { //Your class-private names
    foo: Symbol() //From your intentions
  }
  with(privateNames) { //Ensures your class private names are available without this
    function _myClass() {
      privateScope.put(this, {
        [foo]: "bar"
      });
    }
    myClass.prototype.doSomethingWithFoo() {
      let pvt = privateScope(this);
      // foo is accessible here, but not accessible to methods of this or any other object
      // defined outside the class definition 
      doSomething(pvt[foo]);
    }
    equals(other) {
      let retval = false;
      if (other instanceof myClass) {
        let pvt = privateScope(this);
        let other_pvt = privateScope(other);
        retval = pvt[foo] === other_pvt[foo];
      }
      return retval;
    }
  }
  return _myClass; 
})();

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.

@rdking
Copy link
Author

rdking commented Jan 10, 2018

@littledan Let's see how my suggestion compares with your FAQ.

  • Why aren't declarations private x?
    Now they would be. Many who've taken the time to chime in have honed in on how awkward #x is as a declaration. Your response has consistently been "you'll get used to it." Isn't the same true for private x as a declaration? I think Javascript developers are more likely to quickly adopt syntax that doesn't evoke a negative visceral response. The notion that private foo declares a class-private name fits the context of a class. The notion that private foo="bar" declares the class-private name foo and sets a default value of "bar" on every instance's private scope at that name is close enough to what developers expect to be easily adopted.

  • Why isn't access this.x?
    I get this, though I don't agree with it. Ignoring that, since private x essentially declares a new Symbol x within the scope of the class, instances would not be able to use this.x unless it was public. This is already a part of how Javascript works. No changes required. The syntax for access would be this[x]. The only thing the interpreter would need to know is whether x is a private name in the current scope. This check can be done with negligible effect on performance at the end of the normal scope-chain walk to find x.

  • Why does this proposal allow a class to have a private field #x and a public field x?
    The approach I've taken doesn't remove the possibility of having both a private and a public field with the same name. While the public field would be accessible via this.x or this["x"], the private field would only be accessible via this[x] with no quotes.

  • Why not do a runtime check on the type of the receiver to determine whether to access the private or public field named x?
    As stated before, the standard scope-chain walk, with a check a type-check at the end takes care of this with negligible effect on performance or complexity.

  • Why doesn't this['#x'] access the private field named #x, given that this.#x does?
    No longer an issue given that the semantics would follow current Javascript notation.

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 class is syntactic sugar, and with significantly less workload for the engine developers.

@bakkot
Copy link
Contributor

bakkot commented Jan 10, 2018

Why isn't access this.x?

I get this, though I don't agree with it. Ignoring that, since private x essentially declares a new Symbol x within the scope of the class, instances would not be able to use this.x unless it was public.

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.

@rdking
Copy link
Author

rdking commented Jan 11, 2018

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

class A {
  private x;
  constructor(foo) {
    this[x] = foo;
  }
}

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 x is a class-private name (a Symbol only available to the members declared within the class), the this.x syntax will only ever refer to a public field of this. No changes to Javascript as it currently exists are needed to make that true. As such, the question "Why isn't access this.x?" becomes moot. I apologize if I didn't make myself clear the first time.

@bakkot
Copy link
Contributor

bakkot commented Jan 11, 2018

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.

@rdking
Copy link
Author

rdking commented Jan 11, 2018

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.

@mbrowne
Copy link

mbrowne commented Jan 14, 2018

@rdking Do you have a technical argument against the # symbol or do you just dislike the syntax for aesthetic reasons? Also, I question this phrase:

more palatable to the existing Javascript developer base

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 private keyword and that it's actually doing something different than private in other languages (most languages only have soft private; the proposal here is for truly private properties). And it's short :) (haha, not much of an argument, but my hands/wrists will be happy about it).

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.

@rdking
Copy link
Author

rdking commented Jan 14, 2018

I just made a post here describing more precisely my proposed changes to the syntax, implementation details, and how protected could be implemented following the same paradigm.

@rdking
Copy link
Author

rdking commented Jan 14, 2018

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 private in different languages, you're only partially correct saying that its use is different. For other languages using the private keyword in the context of a class, they often have an explicit compile phase where the code is translated into binary. That phase is complete time and space separate from the run phase. With Javascript, the compile/interpret phase is not so distinct, not defined by the language, and completely implementation dependent.

If you're wondering why that's relevant, it's because private has a different meaning in both of these phases. For compiled languages, private means "don't generate any code that can access this member from outside an instance or the class itself." This means all the work of determining access happens at compile time. Fields are generated and access code is generated, all before even the first instance is created.

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, private means "prevent anything outside an instance or the class itself from a) knowing the field exists and/or b) accidentally accessing the field." PHP is a standing example of an interpreted language with private members in their classes. And before you say that PHP is a bad language, it's not a problem with PHP as much as it's libraries and frameworks. They suck major. The language itself isn't bad at all.

This is the primary reason why there is and should be a difference in "meaning" between private from other languages and what private would be in ES. You also evoked, the "you'll get used to it" argument. Again I ask, isn't the same going to be true for the somewhat different use of private? Wouldn't it be easier to adopt since it's more ergonomic and in line with expectations?

Now for the fun part...

The first technical reason not to use the sigil, and to use this[pName] instead of this.#pName for access goes back to my rather brief discussion with @bakkot about the use of array notation. First, there's the simple fact that by definition, class is syntactic sugar. There is nothing that class does that cannot be done in ES without it. Even the fact that you cannot re-declare a class comes from the fact that you cannot re-declare a function. So technical reason #1 is because it's so much easier to implement since it doesn't require changes to access semantics, and allows class to continue being syntactic sugar. Everything I'm suggesting can be implemented in de-sugared form in ES5. This means babel can support it and TypeScript get's better support for private. With the sigil notation, class would do things that could not be done without it.

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:

  1. the non-intuitive notation
  2. the fact that the sigil specifies that "member" is effectively a Symbol only accessible within the scope of the class Test declaration
  3. and that all accesses to it must include the sigil.
    a. This means that the sigil is overloaded. In the declaration, the sigil means the same as private in my version. However, in access attempts like this.#member, the sigil is there as though it is part of the property name.
    b. Even though it will be documented to the contrary, many will still want to type this['#member'] and expect it to work. While this would be consistent with the way ES currently works, this mistake would instead create a new public member named "#member" only accessible through [] notation.

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:
1. the non-intuitive notation, Not an issue, it's the keyword we were expecting to use!
2. the fact that private specifies that "member" is actually a Symbol only accessible within the scope of the class Test declaration. It's the same learning curve here.

The fact that access to "member" must be done using this[member] is part and parcel of getting used to (2). It's already a well known fact that to access a Symbol key of an object, you must use [] notation. So there's nothing new to get used to with access semantics. That means, compared to the 3 or 4(depending on how you choose to count it) issues to get used to with sigil notation, there is only 1 such issue with my suggestion, and it's something you'd have to get used to with sigil notation anyway.

There's a 3rd reason as well, and it goes back to @bakkot comment:

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.

He was referring to the potential to conflate this[member] with this["member"]. As I pointed out to him, this potential already exists due to the introduction of the primitive type Symbol. As a matter of due diligence, I'll state that a colleague pointed out to me that my suggested syntax would expand the likelihood of someone making a mistake of this type. I couldn't refute his argument. However, during the discussion we both came to the conclusion that sigil notation actually makes the situation worse by introducing the opposite issue.

Since sigil notation requires access to be this.#member, the average ES developer who has grown accustomed to this.<whatever> being equivalent to this["<whatever>"], are equally as likely to make that mistake as the one mentioned about my notation. Since both cases accidentally create a public member, and both are reasonably close to equally likely, this argument is a wash. However, those remembering that #member created a Symbol or Symbol-like name, may also be tempted to try this[#member] which would fail as a syntax error, or this[member] which would always work by creating a new public property on this. However, there is a grave potential for disaster here since, if member is not defined, the new public property will be named "undefined", but not as a string! This is valid in JavaScript.

@mbrowne
Copy link

mbrowne commented Jan 15, 2018

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

@borela
Copy link

borela commented Jan 15, 2018

While ugly, the # is easier to explain to new programmers:

If you want to refer to a private member, add a # in front of it.

vs

If you want to refer to a private member, you need to pass it to this[x] because the private member is actually a symbol and you can't access it directly...

On the former the new programmer would just accept the "magic", on the later you would have to explain why.

@rdking
Copy link
Author

rdking commented Jan 15, 2018

@borela I think you might be making a bad assumption there, namely that private member or #member declares a private field named member. As explained in the FAQ for the private-fields proposal, the goal is to define a "class-private name". I'm not suggesting we change that fact. In fact, I think it's the correct approach for implementing private fields for the ES class declaration. I just don't agree with the syntax choices (since it breaks with existing convention and adds new issues) and implementation details (since it causes class declarations to depart from being pure syntactic sugar).

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: Symbol. This is the main part of the explanation of my preference for this[x] over this.#x. I think trying to explain why the sigil notation is this.#x without being able to this.["#x"] as is standard for ES would prove more confusing to new programmers.

@mbrowne
Copy link

mbrowne commented Jan 15, 2018

I think you might be making a bad assumption there, namely that private member or #member declares a private field named member.

Well, #member does create a private field named #member. I assume you meant it doesn't literally create a property with a string key of 'member'.

BTW here's the link to the FAQ @rdking mentioned: https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md

@borela
Copy link

borela commented Jan 15, 2018

@borela I think you might be making a bad assumption there, namely that private member or #member declares a private field named member.

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

@rdking
Copy link
Author

rdking commented Jan 15, 2018

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 this["#x"] nor this[#x] work, which is inconsistent with current ES.

@borela
Copy link

borela commented Jan 15, 2018

Agree, not being able to do this["#x"] is counter intuitive.

@rdking
Copy link
Author

rdking commented Jan 15, 2018

The only problem is that is in fact necessary that with sigil notation this["#x"] should be invalid. The problem is that the engine would not have any way to know whether or not "#x" was referring to a public or private field. The action taken by the engine would be to create a public field on this named #x. No error would be thrown since this is still a valid action. I mentioned this in my TL;DR post about the technical issues with sigil notation.

@littledan
Copy link
Member

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.

@rdking
Copy link
Author

rdking commented Jan 16, 2018

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

  1. make private names not be property keys of the instance - As you can tell from the de-sugared notation, this[x] is translated to privateScope.get(this)[x] iff (read "if and only if) x is a private name in the current scope. This ensures that
  2. Proxies cannot observe reads and writes to private fields, since the private fields are properties of an entirely separate object. With the above translation being made,
  3. all square bracket access [can] go through Proxy traps, but since the Proxy will have this as a target instead of privateScope.get(this), it is assured that private field access is protected from Proxies.

Barring your goal? of not using square-bracket notation, the suggestion I've provided satisfies all of them, and without:

  1. introducing non-standard, non-intuitive syntax (#x vs private x)
  2. breaking existing syntax conventions (#x vs this.#x and without this["#x"] and this[#x])
  3. elevating class from the status of syntax sugar (internal slot usage)

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:

Unless explicitly specified otherwise, internal slots are allocated as part of the process of creating an object and may not be dynamically added to an object. Unless specified otherwise, the initial value of an internal slot is the value undefined. Various algorithms within this specification create objects that have internal slots. However, the ECMAScript language provides no direct way to associate internal slots with an object.

Your current description of implementation for private names would cause you to need to either:

  1. allocate a private slot on instance creation to contain an object that will hold all of the private fields/methods, or
  2. allocate a new private slot the first time each private name is used on an instance.

If your implementation chooses the 1st approach, there's no issue save for the fact that now class cannot be treated as syntax sugar. However, if the 2nd approach is what you intend to use, then class would be the first keyword to require the use of dynamically allocated internal slots.

I think I've either shown, or can show that:

  • All of the issues I've mentioned over the past few posts are cleanly avoided with my suggestions.
  • The vast majority of your intentions are preserved.
  • Implementation of my suggestion would be easier for engine developers since it requires significantly less changes to the engine.
  • The learning curve for my suggestion is lower than that of your proposal.
  • My suggestion opens the door to a relatively easy implementation of protected where your proposal increases the complexity.

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.

@mbrowne
Copy link

mbrowne commented Jan 16, 2018

A lot of good points here, just wondering about this:

My suggestion opens the door to a relatively easy implementation of protected

How so?

@rdking
Copy link
Author

rdking commented Jan 16, 2018

@littledan There's one other thing I wanted to know about your private name related proposals. Below I've written a class declaration and its de-sugared equivalent. I understand that with your proposal, there will be no direct de-sugared equivalent. Using "[[ ]]" notation for each internal slot you'd create/use, can you mock up what the equivalent code (with & without sugar) would be for your proposal?

/* 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.

@rdking
Copy link
Author

rdking commented Jan 16, 2018

@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 class, not enumerable, with a Symbol for a key. If that Symbol is an internal value in the engine, not exposed to developers, that would reduce the risk of the names being discovered. However, it doesn't really matter even if the names are discovered since they'd be completely unusable for accessing the private/protected fields of an instance.

@littledan
Copy link
Member

Is "not use square bracket-based access" actually a goal by itself?

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 x[y] into something else would seem to make the object model more complicated in a way that I don't quite understand. When do you decide to perform the desugaring, and when is it left as ordinary property access?

@rdking
Copy link
Author

rdking commented Jan 16, 2018

Most of the de-sugaring would happen at parse time, barring the usual hiccups like use of eval() or new Function() which already force re-evaluation of code. The determination of which property access to use is done when walking the scope chain to find a variable reference. If the variable retrieved is a private name, then the corresponding privateScope object is accessed instead of this. In general, barring the use of eval() and similar, all such translations should be possible at parse time. I grant that there may be exceptions that I might not be aware of at present.

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.

@borela
Copy link

borela commented Jan 16, 2018

@rdking
The biggest issue for me with this method would be syntax highlighting, I coded a syntax to color es in sublime text (fjsx15), and, like other editors, sublime use regexes to add scopes to the tokens; the # makes it easy to set the scopes for the private members and giving them a different color on the color schemes.

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.

@littledan
Copy link
Member

I don't understand what the advantage of this proposal would be. It seems confusing to have [] mean something completely different based on a static view of the scope chain. (Note, in spec logic, the scope chain is evaluated at the same time as the rest of the code runs, though maybe we could work out enough of the cases here since with isn't available inside classes.)

@rdking
Copy link
Author

rdking commented Jan 16, 2018

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 this, then there is still no way for a Proxy to monitor private field usage. I don't have any particular qualms about evaluation at runtime. However, I do believe that resolution should occur as early as possible to give optimizer logic more time to get a better result.

Where you say the use of [] seems confusing, I think it's more intuitive.

  1. For both of our syntax, a private name declaration declares something Symbol-like. ES already defines the use of a Symbol as a key to be obj[Symbol]. This is already well known.
  2. While neither syntax allows for private fields to be members of their corresponding instance objects:
    a. your syntax introduces new notational style with limited scope while breaking existing syntax symmetries for non-intuitive(albeit logical) reasons.
    b. my syntax does not introduce any notational changes and does not break any notational symetries

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 private keyword. Everything else about my suggested syntax is a pre-existing consequence of how that meaning is expressed in ES code. As such, simply by learning this:

private is used to declare a new Symbol within the scope of a class definition that is used to access fields on an instance of that class not available to anything that is not part of the original class definition

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 private replaced with # and Symbol with private name, is actually greater using your syntax. If you read my previous posts, I've also mentioned some of the gotcha's that come along with your syntax.

In the end, the use of sigil notation introduces more problems than avoiding [] notation solves.

@littledan
Copy link
Member

I think it's more confusing to have two completely different meanings for [] punned onto the same token than to syntactically differentiate them with a # token. The mental model overhead seems higher here. I don't plan to make this change.

@rdking
Copy link
Author

rdking commented Jan 16, 2018

@littledan That is disappointing.
In a sense that programmers readily understand from its use in other languages, there are not 2 different meanings. [] means "return the value from the variable whose name is between the braces from within the scope described by the calling object". Since both the public scope and private scope are both "described by the calling" instance object, there's very little in the way of "mental overhead". From a developer's POV, it just looks like you're accessing a private instance member that cannot be detected from outside the class definition: one of your primary goals, and well in keeping with the usage of private in other languages.. Further, using your same terminology, you have "three completely different meanings for # punned onto the same token":

  1. outside of any class method, #x means the same as private x (a declaration modifier)
  2. inside a class method, #x means this.#x (a short-hand local scope variable)
  3. inside a class method, this.#x means this.[[PrivateScope]][x] (a short-hand object variable)

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:

  1. destroying the fact that class is syntactic sugar
  2. introducing non-intuitive (or at least very unexpected) syntax
  3. introducing new points of confusion in the language
  4. making the language with respect to class less extensible by making it harder to implement more class-related keywords

just to avoid what you think is higher "mental overhead" for people who are arguably already accustom to both the private keyword and ES object access methods? If so, then I have no choice but to say to you (in an admittedly cheeky fashion) "they'd get used to it." :)

@mbrowne
Copy link

mbrowne commented Jan 17, 2018

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 [] notation:

class Demo {
  foo
  private bar
  
  method() {
    //works
    this.foo
    //works
    this['foo']
    
    //does not work as expected
    this['bar']
    //works, but is non-intuitive
    this[bar]

    const propertyName = 'foo'
    //usual usage without quotes has different meaning
    this[propertyName]
  }
}

@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 private modifier worked neatly, but if it requires overloading the [] notation I don't think it's worth it. Having said that, I'm not sure I fully understand what you said here:

  1. inside a class method, #x means this.#x (a short-hand local scope variable)
  2. inside a class method, this.#x means this.[[PrivateScope]][x] (a short-hand object variable)

I'm guessing that in number 2 in the above list, you're referring to the fact that you can do this:

get #x() { return #xValue; }

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.

@rdking
Copy link
Author

rdking commented Jan 17, 2018

@mbrowne As a matter of clarification:
I'm not particularly tied to [] notation for private field access. I just think it makes the most sense in the face of the current ES specification and the desired goals. ES objects currently use [] as a method of accessing all keys accessible via the calling object. In comparison, . is limited to accessing keys that are strings and meet the qualifications to be a variable name. By definition, anything accessible via . is accessible via []. On the other hand, the same is not true of the opposite. Since it's already the case that [] is used to access non-variable-name-compatible and non-string keys, it makes logical sense to continue that for private names, which also won't be variable-name-compatible strings.

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 class in ES damaged unnecessarily. No matter what kind of approach is chosen, shoehorning this kind of feature into a prototype-based language is going to have an awkward spot with a learning curve for developers if the proposed features are to be implemented. I would rather that the surface area of that awkwardness be as small as possible to the ES specification and engines.

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 this[bar]? Would anyone who understands ES
Symbol not understand that this[bar] !== this['bar']? On this front, my suggestion offers the same level of risk of confusion. That problem came into existence with the birth of Symbol, but I don't think that is the issue. If I'm understanding correctly, the complaint is that this[bar] under my suggestion could mean either [[PrivateScope]][bar] or this[bar] depending on whether or not bar is declared private bar in the current scope.

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:

private is used to declare a Symbol within the scope of the class declaration that is used to access a property in the private scope of an instance.

Except for the part about the declaration being a Symbol which, as stated before, should already be something ES developers are familiar with using, nothing else about that explanation departs from what developers expect from private in other languages.

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 class from the status of syntactic sugar, which is undesirable given the current documentation.

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, class instances will necessarily become "exotic objects". That's completely unnecessary, and may introduce breaking behavior in existing code if not carefully planned.

@bakkot
Copy link
Contributor

bakkot commented Jan 17, 2018

@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 class instances into exotic objects, and has no effects on any existing code (except of course consumers of JS source text, like parsers).

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.

@rdking
Copy link
Author

rdking commented Jan 17, 2018

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 Symbol for the private names. Either way, it's moot since I'm no longer pursuing this suggestion. :)

@bakkot
Copy link
Contributor

bakkot commented Jan 17, 2018

If the Symbol declared by private foo does not cause a prototype chain walk when used in this[foo], it's certainly a different kind of thing than the Symbols we currently have.

@borela
Copy link

borela commented Jan 17, 2018

(and some of the counter-arguments like @borela's comment about syntax highlighting I don't find very convincing).

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

@rdking
Copy link
Author

rdking commented Jan 17, 2018

@bakkot The theory is that with this[foo], since the thing in the braces is not a string, it must be a variable. That means a scope chain walk of the function scope has to be done to find it. When it's found, a quick check determines whether or not it is a private name. It is then evaluated in accordance with that result as this.[[PrivateScope]][foo] for private names, and this[foo] for non-private names. Except for the walk of the function scope chain (which should already exist), it's likely very similar to the parse check you've put into v8 to see if # is the first character of the key name on access via ..

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

5 participants