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

Can we take a step back? #144

Closed
rdking opened this issue Oct 14, 2018 · 254 comments
Closed

Can we take a step back? #144

rdking opened this issue Oct 14, 2018 · 254 comments

Comments

@rdking
Copy link

rdking commented Oct 14, 2018

When there is so much feedback on this proposal from those who are aware and choose to speak at all and from members of the TC39 board itself that there are several things wrong with this proposal that make it a less than adequate solution to the problem of adding data privacy to ES, isn't that a good enough indication that it may be time to step back and re-think?

How about, instead of trying to push through with class-fields, we come up with "class properties" instead? The general idea is simple. Objects have properties. Functions are objects. Classes are functions with a prototype object. Therefore classes should support declaring properties for both its function and its prototype.

Can we solve and add this to the language first, separately from all other concerns? No private, no protected or other intermediate. Just ordinary public and static properties. I'm beginning to come to the impression that until this much is solved, the path forward for a reasonably acceptable, non-future path blocking proposal won't easily reveal itself. Maybe we're trying to take too big of a jump all at once.

@rbuckton
Copy link

You can accomplish this with fields and decorators:

// class fields + decorators
function proto(memberDesc) {
  if (memberDesc.kind !== "field" ||
      memberDesc.placement !== "instance") throw new Error("Only valid on non-static fields");
  memberDesc.placement = "prototype";
}

class C {
  @proto propertyOnPrototype = 1;
}

The biggest issue with declaring data properties other than methods on the prototype is related to shared instances of non-primitive values:

class C {
  state = { counter: 0 }; // on instance
  incr() { console.log(this.state.counter++); }
}

class D {
  @proto state = { counter: 0 }; // on prototype
  incr() { console.log(this.state.counter++); }
}
const c1 = new C();
c1.incr(); // prints: 0
c1.incr(); // prints: 1

const c2 = new C();
c2.incr(); // prints: 0
c2.incr(); // prints: 1

const d1 = new D();
d1.incr(); // prints: 0
d1.incr(); // prints: 1

const d2 = new D();
d2.incr(); // prints: 2
d2.incr(); // prints: 3

In most OOP languages, state shared between instances is placed on the "static" side of the class.

@rdking
Copy link
Author

rdking commented Oct 14, 2018

@rbuckton I think you may have missed the point of what I'm saying. Right now class is equivalent to a constructor function and a prototype with all defined prototype elements defined as non-configurable, non-enumerable, writable. If doing this manually, data properties can be placed on the prototype with default values. The problem of assigning a property an object for a default value has always existed. This makes it a non-issue in my book. However, in a class, there is no direct means of making the same assignment.

I'm saying that this:

class Foo {
  x = 1;
  inc() { ++this.x; }
}

being an equivalent for:

const Foo = (function() {
  var retval = function Foo() {};
  retval.prototype = {
    constructor: retval,
    x: 1,
    inc() { ++this.x; }
  };
  return retval;
})();

needs to be implemented before we go any further with any kind of private data proposal. Let's get public working first.

@jridgewell
Copy link
Member

with all defined prototype elements defined as non-configurable, non-enumerable, writable

Nit: They're configurable, writable, and non-enumerable.

The problem of assigning a property an object for a default value has always existed. This makes it a non-issue in my book. However, in a class, there is no direct means of making the same assignment.

This is an anti-pattern, and should be avoided at all costs. It's been the source of countless bugs in Backbone (which has a very similar class system).

That class fields syntax does not allow you to do this natively is a great positive. If you want to shoot yourself in the foot later, mutate the prototype after the class' curly brace.

@mbrowne
Copy link

mbrowne commented Oct 15, 2018

I think having a way to declare instance properties is much more important than a dedicated syntax to declare prototype properties. The only way this could work without causing major usability issues would be for it to detect non-primitive (object) initializers and set those to null on the prototype and set the initializer value on the instance in those cases...which sets up an inconsistency. I’m not saying it’s totally unworkable but the current proposal is much simpler (at least if we’re just comparing public properties) and between static and decorators we’ll have all the power we need to affect placement.

This idea would indeed be taking a step back, but not for the better...

The following comments might be a better fit for the other thread but...The problem with the “maximally minimal” approach is:

  1. JS has been suffering from the lack of proper encapsulation for so many years already and it seemed that in past proposals something else was always a higher priority. It’s time to finally make it happen. If private fields were removed from this proposal, how much longer would we have to wait? If it would be 6 months after the ratification of this proposal, perhaps that would be acceptable but isn’t it hard to know how long it might take? Perhaps years, especially given how controversial this clearly is...

  2. Public and private fields/properties are closely related and it makes sense to consider both and release them together. I would say this extends to access control on general if it weren’t for number 1 above. But this is why I keep insisting on a general plan for how other access levels might be handled, because the decisions made now - in some cases even decisions just related to public properties/fields - will very significantly affect any future proposals. BTW I think it would be great if this proposal and decorators were released very close together, because hard private on its own with no way to modify or expose it for legitimate use cases could cause lots of problems.

@hax

@rdking
Copy link
Author

rdking commented Oct 15, 2018

There's a much more straight forward solution than not using the prototype at all. In much the same way as ** doesn't allow non-constant expressions as the LParam due to the multiple ways get the wrong result, simply disallow assignment of an object to a class property definition unless the property is static. Cleanly solves the problem, that is, until you have to deal with variables. Then things get messy. That's why the real suggestion is translating this:

class Foo {
  x = {foo: 'bar'};
  print() { console.log(JSON.stringify(this.x, null, '\t')); }
}

into something like this:

const Foo = (function() {
  const isSet = Symbol()
  var retval = function Foo() {};
  retval.prototype = {
    constructor: retval,
    get x() {
      if (!(this && (typeof(this.x) == "object") &&
          this.x[isSet])) {
        this.x = { foo: 'bar' };
      }
      return this.x.value;
    },
    set x(value) {
      this.x = {
        [isSet]: true,
        value
      };
    },
    print() { console.log(JSON.stringify(this.x, null, '\t')); }
  };
  return retval;
})();

The general idea here is to defer the setting of properties just like is being done for the current proposal. The only difference is that the property appropriately lives on the prototype, as is expected.

@mbrowne
Copy link

mbrowne commented Oct 15, 2018

@rdking

The only difference is that the property appropriately lives on the prototype, as is expected.

As is expected by whom? Even before JS had classes I was already declaring data properties in the constructor and methods on the prototype. It would only be in special cases where I added data properties to the prototype. And while of course some people did things differently, this seemed to reflect most of the examples and common practice. And now many people have already been using public fields via Babel and having no problems with them, and the default placement of an instance property seems to work well for people.

@rdking
Copy link
Author

rdking commented Oct 15, 2018

@mbrowne As you should be able to see from my previous post, there's a solution that gives you all the benefits of instance "fields" without going all the way to the "field" abstraction. I can't really see any reason why we need to go as far as something like "fields" to do what ES can already do with properties.

@rdking
Copy link
Author

rdking commented Oct 15, 2018

@mbrowne There are going to be those who do things as you do, and those who do things as I do. The two groups will likely never be reconciled. However, the point is to take an approach that allows for both methods to be used. Embedding an approach that only considers one way or the other does a disservice to those using the opposite approach. What I've suggested above gives both.

@hax
Copy link
Member

hax commented Oct 15, 2018

@jridgewell

If you want to shoot yourself in the foot later, mutate the prototype after the class' curly brace.

But current proposal just shoot yourself in another direction.

Code sample which I already pasted several times and no one respond:

class ExistingGoodClass {
  get x() {...}
  set x(v) {...}
}
class NowTrapAgain extends ExistingGoodClass {
  x = 1
}

Note, this footgun have different variant forms if you considered how code evolve.

@rdking
Copy link
Author

rdking commented Oct 15, 2018

@hax @jridgewell The solution I'm suggesting avoids the gun altogether. That example would translate to

class ExistingGoodClass {
  get x() {...}
  set x(v) {...}
}
var NowTrapAgain = (function() {
  function getGetter(obj, field) {
    var retval;
    while ((typeof obj == "object") && !obj.hasOwnProperty(field)) {
      obj = Object.getPrototypeOf(obj);
    }
    if (obj.hasOwnProperty(field)) {
      retval = Object.getOwnPropertyDescriptor(obj, field).get;
    }
    return retval;
  }

  return class NowTrapAgain extends ExistingGoodClass {
    get x() {
      var data = 1; //This is the original assigned value
      var getter = getGetter(this, "x");
      if (this !== Test.prototype) {
        if (!getter.data) {
          getter.data = new WeakMap();
        }
        if (!getter.data.has(this)) {
          getter.data.set(this, data);
        }
      }
      return (this === Test.prototype) ? data : getter.data.get(this);
    }
    set x(val) {
      /* It can be solved in the engine possibly, but from source, there's no solution that
       * would allow perfect object re-instantiation given a data parameter, so changing
       * the prototype value is out.
       */
      if (this !== Test.prototype) {
        var getter = getGetter(this, "x");
        if (!getter.data) {
          getter.data = new WeakMap();
        }
        getter.data.set(this, val);
      }
    }
  }					
})()

The basic principle here is to split the property into a getter & setter, and allow them to manage the instantiation of the data. Done this way, neither of the foot guns you two mentioned exist.

@loganfsmyth
Copy link

@hax To make sure we're on the same page, that snippet will not trigger the setter, it will define a new property with the value 1. Which behavior do you consider a footgun?

@hax
Copy link
Member

hax commented Oct 15, 2018

JS has been suffering from the lack of proper encapsulation for so many years already and it seemed that in past proposals something else was always a higher priority. It’s time to finally make it happen.

Technically speaking, I agree "proper encapsulation" is the highest priority in all scope of this proposal covered. And I sincerely hope it could happen.

If private fields were removed from this proposal, how much longer would we have to wait? If it would be 6 months after the ratification of this proposal, perhaps that would be acceptable but isn’t it hard to know how long it might take? Perhaps years, especially given how controversial this clearly is...

As a man who already have writing JavaScript for 20 years, and plan to keep writing for another 20 years, I don't care waiting another 2 years, because if you landed a broken solution, I would suffer 20 years.

To be honest, the semantics of the private part of this proposal is ok to me. I even considered the # syntax was acceptable.

But as I already said, after I gave a speech, and got the feedback, I just realized, we should never underrate the risk of the community break. And even I see some other controversial issues in other proposals, the risk of no one can compare to this.

Why?

Because there are already too many solution for private in the wild! If you just land a unwelcome proposal like this, you are just add another mess even you are literally "standard".

This is especially true in TS land, because TS already have an official, solid, programmers familiar private solution. You can see my analysis in other thread.

  1. Public and private fields/properties are closely related and it makes sense to consider both and release them together.

No. They are not. Public is not essential as private. Actually public field has been proved as a bad idea in other OO languages like Java, C#. Though the reason why they discouraged it not necessarily applied to JS, if you checked deeply, you just found more issues in JS than in Java/C#.

Note, the historical property usages in JavaScript is not equal to "public field declaration" you introduced in this proposal. There is a subtle but significant difference, that you provide a footgun that subclass could easily (and accidently in almost all the cases) redefine a property which is already defined by base class. This footgun is never available in the past unless someone explicitly write Object.defineProperty in the constructor --- no one write code like that, and if there is anyone, then that means he should definitely know what he was doing and no regret. I have no idea why we have to introduce such footgun.

@hax
Copy link
Member

hax commented Oct 15, 2018

Which behavior do you consider a footgun?

Yes you know it define a prop, but average programmers just think he do the same as constructor() { this.x = 1 }

@slikts
Copy link

slikts commented Oct 15, 2018

… TS already have an official, solid, programmers familiar private solution.

The privacy in TS does close to nothing, because TS users can just opt-out of types to use private APIs, and it doesn't affect anyone else. Moreover, TS was never meant as a competing standard, so the image is not relevant, and it's getting a bit too spammy.

@hax
Copy link
Member

hax commented Oct 15, 2018

There are some options to solve this:

  1. Add public or any other keyword to make it explicit here is a declaration. Though it's still footgun, at least programmers can see it! But unfortunately you will not like the consequence of add keyword which ask private #x and other syntax storm.
  2. Do not allow initialization, which never confused with assignment. Though it's still footgun, programmers has no reason to use it, so they are probably safe. But I think you will not accept it...
  3. Use constructor() { this.x = 1 } semantic, then it's not a public field proposal 🤣 , it just allow you to move the assignment out of constructor.

I think you don't want to any of them. So just let the footgun shoot ourselves... 😭

@hax
Copy link
Member

hax commented Oct 15, 2018

@slikts

The privacy in TS does close to nothing, because TS users can just opt-out of types to use private APIs, and it doesn't affect anyone else. Moreover, TS was never meant as a competing standard, so the image is not relevant, and it's getting a bit too spammy.

Ok, you can keep your judgment which have no any proof.

Or you should ask TS guys how they think about it.

@slikts
Copy link

slikts commented Oct 15, 2018

Sorry, proof about what? Here's TS playground demonstrating privacy being stripped away in JS and being optional in TS. Here's TS design goals, which include tracking JS.

@rbuckton
Copy link

@rdking: Field initializers cannot be lazy by default as you propose in #144 (comment) as it would cause strange side effects and unexpected behavior:

let idCounter = 0;
class C {
  id = idCounter++;
}
const a = new C();
const b = new C();
b.id; // 0, but expected 1
a.id; // 1, but expected 0

The semantics of field initializers mimic the behavior of field initializers in Java/C#, in that initializers are processed in the constructor after superclass constructors have been evaluated.

Privacy in TS has been well known to be a "soft" private, relying on design time behavior to inform the user that the API is not safe to depend upon. It is not and was not designed as a security feature, but more of a way to indicate to users parts of an application that you should not take a dependency on. It is akin to /** @private */ in JSDoc, only you can rely on it more as it becomes an error during build.

@rbuckton
Copy link

@rdking: Field initializers cannot be lazy by default […]

Note that you could, however, create a decorator that performs lazy initialization. However, unlike the current TS behavior (which uses Set and would trigger setters on the prototype), the class fields proposal uses CreateDataPropertyOrThrow (which won't trigger setters on the prototype).

@hax
Copy link
Member

hax commented Oct 15, 2018

Sorry, proof about what? Here's TS playground demonstrating privacy being stripped away in JS and being optional in TS. Here's TS design goals, which include tracking JS.

Ok if you are talking about hard private, I never against it. I just want to say, the difference between hard private and TS compile-time private doesn't very important in practice, at least in most cases.

And, I think TS eventually should move to one private, it should be JS native private, as you say, the design goal of TS is to follow JS semantic.

Because of that, you should realize the good proposal should not only consider JS, but also consider TS.

If a proposal can not satisfy TS programmers or bring them pain, they will keep using TS compile-time private.

Could you get my point?

@hax
Copy link
Member

hax commented Oct 15, 2018

@rbuckton

The semantics of field initializers mimic the behavior of field initializers in Java/C#, in that initializers are processed in the constructor after superclass constructors have been evaluated.

I'm afraid C# has different order with Java. I prefer C# but it seems current proposal choose Java?

@rbuckton
Copy link

@hax: How do they differ? While I admit I've spent considerably more time in C# than Java, the initialization semantics in https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.5 seem fairly similar to C#'s semantics.

@mbrowne
Copy link

mbrowne commented Oct 15, 2018

class ExistingGoodClass {
  get x() {...}
  set x(v) {...}
}
class NowTrapAgain extends ExistingGoodClass {
  x = 1

IMO it would be better if redeclaring the same property on a subclass in this way were illegal. It’s a recipe for confusion and problems. Overriding the getter and setter would probably be OK but overriding it with a public field declaration is an issue. I’m also against this:

class Superclass {
  x = 1
}
class Subclass extends Superclass {
  x
}

@rdking
Copy link
Author

rdking commented Oct 15, 2018

@rbuckton Given your example, I'd expect a.id === b.id && a.id === 0 && idCounter === 1. My reason? I expect a definition to be descriptive, not prescriptive. Put another way:

let idCounter = 0;
let C = function() { //There's no constructor in C! }
C.prototype = {
  constructor: C,
  id: idCounter++;
};
const a = new C();
const b = new C();
b.id; // 0
a.id; // 0

Justification? Currently everything inside the class definition goes on the prototype of the class, including the constructor function. The class keyword is just sugar for building a prototype with a default constructor if one isn't provided, and then returning the constructor. Adding data property definitions to the class keyword shouldn't suddenly up and do something different. Besides, accessor property definitions end up on the prototype already, right?

@rbuckton
Copy link

@rdking: That is not how class fields behave in any other OOP language. If those initializers defined properties on the prototype, the feature would be a major foot gun and pretty much unusable. Prototypes should define instance shared behavior. Even with classic ES5/3 "classes", defining non method or accesor members of a shared prototype was a foot gun and generally to be avoided.

@rdking
Copy link
Author

rdking commented Oct 15, 2018

@mbrowne From a different point of view, I agree that

it would be better if redeclaring the same property on a subclass in this way were illegal.

But the problem is that the re-declaration is a "field" and not a property. If the assignment remained in the constructor, there would be no problem. However, assigning a value in a definition sets up the expectation that the value will be present before the first line of constructor code is run (that includes super()).

Put simply. If you want an instance field, then make it after the instance is created (i.e. in the constructor). However, if you want a class property, it needs to be part of the prototype. There's no issue with overriding a base class property with a derived class property. That's the very basis of prototype inheritance.

@rdking
Copy link
Author

rdking commented Oct 15, 2018

@rbuckton You might want to double-check your facts. Both C++ and Java work that way. Values assigned to properties of a class are done so statically so that an internal prototype of the exact shape of a class instance can be created. This is why sizeof works. The resulting assignments to the properties of the instances created are the values that were captured from the definition. If you wanted to assign a value that's dynamic to a field, you have to do it from the constructor.

@bakkot
Copy link
Contributor

bakkot commented Oct 15, 2018

@rdking:

import java.util.*;
import java.lang.*;
import java.io.*;

class Main {
	static int idCounter = 0;
	static class C {
		public int id = idCounter++;
	}
	public static void main (String[] args) {
		C a = new C();
		C b = new C();
		System.out.println(b.id);
		System.out.println(a.id);
	}
}

output:

1
0

Try it.

sizeof does not need to know the actual value of anything, only its type.

Sidebar: I'd really appreciate it if you would create and run (and ideally provide) code samples before making claims about the behavior of existing languages.

@rbuckton
Copy link

rbuckton commented Oct 15, 2018

This is a big difference between a typed language like C# and an untyped language like JavaScript. sizeof in these languages works because the metadata about the shape of the class (the declared members and types) exists as part of the class declaration. The actual values are not initialized until such time as the constructor runs.

The difference between ECMAScript and C# constructors, is that C# does not allow you to evaluate Statements prior to calling the base class constructor:

// c#
class C {
  public int x;
  public int y = 1;
  public C(int x) {
    this.x = x;
  }
}
class D : C {
  public D(int x): base(x) {
    // cannot execute statements before `base(x)` is evaluated.
    Console.WriteLine(this.x);
  }
}

vs

// js
class C {
  x;
  y = 1;
  constructor(x) {
    this.x = x;
  }
}
class D extends C {
  constructor(x) {
    // can execute statements here, but `this` is not initialized yet.
    super(x);
    console.log(this.x);
  }
}

@bakkot
Copy link
Contributor

bakkot commented Dec 11, 2018

Sorry, the question wasn't there when I wrote my reply. Anyway, you'd have to ask whoever made that change; I don't believe such a change was discussed in committee. (And at a glance it looks to me like decorators still imply [[Define]] semantics, so I'm not sure what you're referring to.) All I was claiming was that the question of which semantics to use for class fields was indeed discussed, at length, before stage 3, and TC39 came to consensus on [[Define]]. Otherwise it would not be at stage 3 with those semantics.

@bakkot
Copy link
Contributor

bakkot commented Dec 11, 2018

Oh, sorry, I misread - yes, it's proposed that decorators would allow [[Set]] semantics as an opt-in. This seems sensible to me. The default, and the semantics for undecorated fields, remains [[Define]]

I guess I don't understand what you're asking.

@Igmat
Copy link

Igmat commented Dec 12, 2018

@ljharb but what if some issues appeared only AFTER consensus and those issues may lead to objections from some committee members if they knew them BEFORE?

@littledan
Copy link
Member

To respond to the original post: I think private fields (and methods) are well-motivated to provide encapsulation. I understand if you want them to be delayed, but I think we've thought this through well and have a good design. Advancing the proposal to Stage 3 signified that TC39 agreed, and I've heard a lot of positive feedback from JavaScript developers who I've met, even if the discourse in this repository is more negative.

On the Set vs Define question, which this thread seems to have veered into: please see a description of the resolution of this issue in the explainer.

@rdking
Copy link
Author

rdking commented Jan 4, 2019

@littledan

I think private fields (and methods) are well-motivated to provide encapsulation.

No one has doubted the "motivation". We are all here discussing the issue because the motivation is undeniably good! We simply believe that the chosen implementation leaves far too much on the cutting room floor in trade for the scarce few benefits we stand to receive. The simple existence of other possible implementations that leave far less damage in their wake should be more than enough to make TC39 want to reconsider their options.

For example, proposal-private-symbols only has 2 issues:

  1. It complicates Membrane a bit, but not so much as to make it unfeasible.
  2. It contains the ability to leak private keys, but this can be as useful as it is dangerous.

At the same time, it has a far smaller feature set than class-fields. However, there's proposal-class-members, which has a superset of the features of class-fields with none of the issues. The only potential issue it can be said to have is that its syntax is slightly more verbose. That was necessary to solve some of the ASI issues caused by class-fields. As an added benefit, the mental model is just the combination of closures and class instances (which I understood to be the conceptual goal of trying to add private data syntax).

I understand if you want them to be delayed, but I think we've thought this through well and have a good design.

Has TC39 thought through class-fields well? There's no doubt the answer is yes. Did you come up with a good design? As viewed within the limits of the paradigm chosen for class-fields, again yes. However, as viewed without such blinders, NO. As I've said many times before, while each individual issue is not enough to warrant even thinking about slowing the progress of class-fields, the combination of those issues greatly exceeds the value we stand to gain from the proposal.

Advancing the proposal to Stage 3 signified that TC39 agreed, ...

This is a process flaw, and an incorrect assessment at the same time. Advancing the proposal to stage 3 merely signified that no one was willing to risk losing political capital over hurt feelings caused by openly vetoing the current proposal. Such a loss risks causing proposals by the vetoing board member to fail gaining traction due to emotionally based counters by the offended parties. Let's call a spade a spade here.

...and I've heard a lot of positive feedback from JavaScript developers who I've met, ...

Of course you'd hear a lot of positive feedback from them. You've given them something they want. Wouldn't you be happy too if someone important in your community handed you a platter of you favorite snack? But how would you feel if 2 hours after eating them, you felt sick on the stomach and had to spend the next 24 hours on the toilet? That's what this proposal is like. It looks good up front, but you won't experience the negative consequences of it until you start trying to use it for something non-trivial. Who's going to use this proposal for something non-trivial while it's still in development?

...even if the discourse in this repository is more negative.

Doesn't the discourse in this repository represent the view of those who feel they have a stake in the outcome? Let me go back to one of your other analogies. If every house on the street has a dog that sleeps outside, but your dog (and only your dog) is barking tonight even though you didn't find anything wrong when you looked, what do you do? You've only got 2 choices:

  1. Get the dog to take you to the problem.
  2. Try to silence the dog.

Right now, TC39 is doing the 2nd. Unfortunately, if you push class-fields to stage 4, even though you're currently on good terms with the neighbors (positive feedback), eventually their dogs will start barking too. Unfortunately for all of us, it'll be too late to fix the problem then.

@ljharb
Copy link
Member

ljharb commented Jan 4, 2019

emotionally based

Opinions based in emotion are utterly valid, for the record. Emotions matter, because we're human beings.

@rdking
Copy link
Author

rdking commented Jan 4, 2019

@littledan

On the Set vs Define question, which this thread seems to have veered into: please see a description of the resolution of this issue in the explainer.

That's nice and all, but you're basically telling us all that in order to fix the inheritance problem TC39 is pushing on us, we're going to have to wait several more years for a proposal (that's already been stuck for several years) to reach stage 4. Until then, the feature is useless to those of us who need to reliably use inheritance. That's not a resolution. That's a brush-off.

@ljharb

Opinions based in emotion are utterly valid, for the record. Emotions matter, because we're human beings.

Please don't take what I say out of context to make your points. I was referring to the type of negative argument that gets raised by a person due mostly to a personal dislike for the opponent. These types of arguments usually never come about except due to an emotional context that has nothing to do with the topic. If there's emotion due to the topic, of course it's valid.

However, much like the varying degrees of dislike and disgust shown for the unfortunate syntax of this proposal, rationality matters more than emotion. I can't argue against the syntax of this proposal no matter how ugly it is. Why? It is the way it is due to limitations of the language and paradigm used by this proposal.

What all of us dissenters have been trying to get through to you of TC39 is that your paradigm is flawed and leads to problems that are going to affect us all negatively. We (including some members of TC39 itself) have been trying to present new paradigms that can accomplish exactly the same goals as this proposal. However, as a whole, the board has not shown much(if any) interest in listening.

It has only been a handful of you that seem to think it worthwhile to even entertain these alternate possibilities, and for that I think we are all thankful. However, the fact that those of you who are indeed listening are either the champions of the current proposal, for which you have an obvious vested interest, or dissenters who are not willing to risk their political capital on the board to veto this not-good proposal, it doesn't do much good for either us as individuals voicing our concerns, or the language and community as a whole who are about to suffer an irreversible slight.

You asked me once before if I would be satisfied if TC39 were to really give all candidate proposals more consideration. I'll answer you here once again. I would not be satisfied if it did not result in the unseating of the current proposal. But I would be at least more content in the knowledge that due consideration was given to the alternatives. If TC39 were to come up with clear, insurmountable reasons why the alternatives are insufficient, then I would no longer have the ability to argue, despite my dissatisfaction. That alone would represent a massive improvement.

@littledan
Copy link
Member

I am not convinced that the private symbol based approach will be viable either, but I have left those issues open since there is a lot of new discussion on it, it will likely be coming to TC39 soon, and I haven't yet documented my concerns thoroughly.

I am not sure if I agree with your analysis of the process flaws, but this is a really complicated issue. I don't think it's quite too many or too few vetoes, but that the use of vetoes (or veto threats) is unequally distributed within the committee, and this influences how tradeoffs are weighed.

@rdking
Copy link
Author

rdking commented Jan 5, 2019

@littledan Admittedly, the private symbol-based approach as defined by @zenparsing, is not as feature-rich as class-fields. At the same time, it is far more self-contained and only suffers from 1 real issue (the fact that it can leak private keys). Even this issue isn't insurmountable and can be handled without additional language modification.

Class fields has numerous issues. Even if each issue individually has a severity of 1/8 the value of this proposal, there are more than 8 issues. The value of this proposal is more than consumed by the issues it contains. Sure, few have run into them with any real severity yet. However, few have actually tried to write any production-level code using class-fields yet. This is one of those situations where your current level of presumption is likely going to come back to haunt you. I hope that's not true, but looking at how many of my current design patterns this proposal is about to break, I don't have that much hope.

I don't think it's quite too many or too few vetoes, but that the use of vetoes (or veto threats) is unequally distributed within the committee, and this influences how tradeoffs are weighed.

Meanwhile, we developers get stuck with something that poses problems we can't even work around, just because of some political bologna slices. What happened to doing things that are in the best interest of the community?


Let me set all of that aside and say this:

I'm glad some of you are giving private-symbols a good close look. I don't have any confidence it will be able to supplant class-fields either. But it's still a better overall proposal.

Have any of you given any thought to class-members at all? Sure, it began as a continuation of classes-1.1, but has since evolved long past that point. The current incarnation supports every capability provided for in class-fields but without any of the trade-offs that we've apparently been re-hashing to death. Even if it cannot be considered as a replacement for class-fields, TC39 should study its principle design. Maybe if you do, you'll figure out how to solve the problems in class-fields without leaning on a proposal that isn't even close to guaranteed to be released.

@bakkot
Copy link
Contributor

bakkot commented Jan 5, 2019

Have any of you given any thought to class-members at all?

I've looked at it some. There didn't seem to much anything in it that hadn't already been discussed at great length.

For example, I think overloading let in this way is a very bad idea; people have enough trouble with closures and scope already without introducing a notion of a closed-over variable whose value changes depending on how the function is invoked. (Note that it is already the case that class X { print(){} } creates exactly one print method, not one for each instance of the class.) And I think a "closure access operator" would be very confusing, am opposed to introducing syntax for adding data properties to the prototype, etc. I imagine you can infer my positions on most design questions from previous discussions we've had.

@hax
Copy link
Member

hax commented Jan 5, 2019

I think overloading let in this way is a very bad idea

If you don't like let, choose other keyword. The essential part is having keyword, which just eliminate the [[Set]] expectation, the ASI hazard and the controversial # syntax.

I never understand why you always use a bikeshed issue to dismiss a proposal but never apply same standard to current broken proposal.

@rdking
Copy link
Author

rdking commented Jan 5, 2019

@bakkot

introducing a notion of a closed-over variable whose value changes depending on how the function is invoked.

Funny. That's not how it works at all. Each instance carries it's own closure in the same way as an object with functions returned from a factory function carries around the closure of the factory function. The analogy is exact. Anyone who understands object factories would also understand this concept. Given that it's a much older concept in ES than class, I don't see your argument. I would like to hear more though.

Note that it is already the case that class X { print(){} } creates exactly one print method, not one for each instance of the class.

And that wouldn't change, so I'm not really sure what you're talking about here.

And I think a "closure access operator" would be very confusing

"Closure access operator", "private member access operator", I don't care what its called. I only named it that because I felt it fit the scenario. If enough people think it's a confusing idea, it can be renamed. The purpose doesn't change though. Again. I'm interested in hearing more of your thoughts.

(I) am opposed to introducing syntax for adding data properties to the prototype

Probably as much as I am opposed to introducing syntax for adding data properties to something that's not a product of the class keyword. But again, that's a negotiable point.

I imagine you can infer my positions on most design questions from previous discussions we've had.

More or less, and we don't disagree as much as you would want to say. Here's a question for you. Given any particular issue (doesn't matter what it is) which is more important, an emotional argument about how just 1 person feels about the issue, or a rational argument about how the technical details of the issue will affect the community?

Like I keep saying, I'm always willing to discuss issues with those who keep an open mind and can speak rationally and logically. Emotion only has a place as an argument when there is no rational or logical argument to be made. Don't "like" the keywords? Change them, but not to symbols because that smacks of operators when we're trying to define declarators.

@bakkot
Copy link
Contributor

bakkot commented Jan 5, 2019

@rdking These two statements cannot both be true:

Each instance carries it's own closure in the same way as an object with functions returned from a factory function carries around the closure of the factory function.

class X { print(){} } creates exactly one print method, not one for each instance of the class.

because the variables a single function sees do not depend on how the function is invoked, which means that if there is only one print, it can only see one value for a given variable which occurs outside of it. In particular, it cannot see different values when invoked on different instances.

Re:

Emotion only has a place as an argument when there is no rational or logical argument to be made.

I would have to have a much longer philosophical conversation which I am not really up for having before I could explain why I don't consider this statement to be well-founded. Please let's just drop the topic of emotion vs logic.

@mbrowne
Copy link

mbrowne commented Jan 5, 2019

@rdking After this comment, I'll avoid interjecting too much in the non-technical parts of this discussion (to avoid adding unnecessary noise), but I just want to point out that your characterization of the quantity and severity of issues in this proposal as it compares to alternatives is highly opinionated. If the committee agreed that alternative proposals had far fewer issues and that those issues were far less significant than those of class fields, then I hope it's obvious that they would not continue to back the current proposal. I sympathize with your frustration at trying to decipher some of the committee members' more terse responses to certain questions, and it's always ok to ask for more clarification, but everyone here has valid reasons for their position even when they don't explain every point at great length. And where they disagree it's because they have a very different perspective from you, not because they're being intellectually lazy or the other things you're implying. It's unfair to conclude that the only possible explanation for their decision is that they're irrationally neglecting to take all the issues fully into account. I'm not sure whether it's your honest opinion or more of a debate tactic to challenge them to explain their reasoning in more detail, but in any case I think it's unfair and really not helpful.

If I were you I would be frustrated too, especially given that there hasn't been more of a direct critique of your class-members proposal (or even a detailed public critique of classes 1.1 that I'm aware of, although I'm sure there was more discussion internally in the committee, and also many of the discussions here have touched on it in various ways). But obviously the committee has spent a great deal of time engaging with you and other members of the community, and I don't know why it's so hard to conceive that they simply disagree; you obviously prioritize things very differently and come from a different perspective for valid reasons just as they have valid reasons. It may seem like I'm saying "everyone has valid reasons" just for the sake of reducing tensions, but that's not it. I genuinely think you're being unfair and uncharitable to paint the committee in the way you have here.

@mbrowne
Copy link

mbrowne commented Jan 5, 2019

@hax

I never understand why you always use a bikeshed issue to dismiss a proposal but never apply same standard to current broken proposal.

The criticism of using closure-like syntax for instance-specific state is not "bikeshedding". For me, it's one of the most important reasons that I prefer this proposal to classes 1.1 and class members. I believe that var or let/const syntax could work as a next-best option for private members, but overall I think the syntax and consistency of class fields for public and private fields is significantly more intuitive than classes 1.1 and class members. It seems that @bakkot and other committee members see things similarly and that this is actually significant enough to be a deal-breaker for var or let/const or (pick your keyword) syntax.

@rdking
Copy link
Author

rdking commented Jan 6, 2019

@mbrowne I get what you're saying, but...
Everyone has opinions that should be respected. My frustrations are not with these opinions. My frustrations are with the unanswered "why" questions of which I have raised many. As a member of the board, @ljharb is an excellent representative in that he does his best to explain the "why" when he can. So I have no issue with him.

As my wife has explained it to me, I have a particular pet peeve for unnecessarily irrational arguments. So you'll have to excuse me if I present a rational or logical argument, get countered with an emotional argument, and find myself frustrated. I am probably incapable of comprehending why an emotional argument would ever be deemed more important than a logical or technical argument when it comes to a communications form like a programming language.

A computer cannot process our emotions. It can barely process our intentions. Even then, it can only process our intentions if they are specified in a clear and rational way. To this end, our emotions should not matter nearly as much as the technical and use case details. First make it work. Then make it look good. You can always make something ugly look good later. That's why this horrible syntax is actually an acceptable issue for me. However, a machine that is broken by design is extremely difficult to fix after its been built.

Do I think TC39 has been intellectually lazy? NO! Not even close! Believe me when I say I understand how much work it takes to make something even reasonably close to workable out of a bad design. I have had to do that at work more often than I care to admit. What I would like are answers as to why there are so many cases where the emotional argument has outweighed the logical or rational arguments that dissented. I already know the opinion of TC39 members. I want to understand those opinions. If I can be helped towards an understanding that TC39 has not made a form-over-function choice (which is what this proposal looks like given what's been revealed so far), then I think you'll find my responses more palletable.

Until such time, understand that despite my penchant for logic and rationality, I too am an emotional being and will show that emotion as I am so moved. In all honesty, I've done well to hold back as much as I have given. I've fought hard against myself to keep the exposure of my frustration to a minimum. I do not believe I'm asking for too much. I'm also not the only one seeking these answers. So I don't believe my frustration to be unwarranted when after so many long years of asking questions, the answers are still not forthcoming.

@bakkot
Copy link
Contributor

bakkot commented Jan 6, 2019

@rdking, I've been doing my best to answer your questions and the questions of other people for several years now. I'm sorry I haven't been able to articulate answers such in a way that we are able to mutually understand each other. I'm not sure what else to do.

@mbrowne
Copy link

mbrowne commented Jan 6, 2019

@rdking

So you'll have to excuse me if I present a rational or logical argument, get countered with an emotional argument, and find myself frustrated.

I don't want to get into a fruitless debate about this, but I have seen very few comments from committee members saying things like, "I don't like it" without providing or citing a logical explanation (however ambiguous or lacking in details). I do get your frustration, but this is more of a communication problem than actual non-existence of the logical reasoning (including the bigger-picture "why" reasoning) you are looking for.

@rdking
Copy link
Author

rdking commented Jan 6, 2019

@bakkot For the first time, I think we feel the same way. 😆 I'd still like to know what type of thing that # is. From what I've seen, even @ljharb thinks of it as a part of the name and not part of an operator (in the access case). But unless your answer is more determinate than what you gave me last time, don't worry about answering. I'm done asking about that. My goal now is to see that when this proposal is finally released, as many of the issues in it as possible are immediately mitigable.

@mbrowne If it's a communications problem, color me stumped as to what got missed. I've worked very hard to analyze every argument given to me. Every bit of new information gets processed. I'm sure the same has been true for the TC39 members I've argued with. If the information I needed were present, I either accepted it or refuted it with more logic. That's just the way I argue. Either way. I guess it doesn't really matter any more.

@littledan
Copy link
Member

If the information I needed were present, I either accepted it or refuted it with more logic.

This is the thing: if you ask a question, get an answer, and then explain why you disagree with it, this is different from not having the "why" presented.

@rdking
Copy link
Author

rdking commented Jan 6, 2019

@littledan I recognize that. Like I said before. The "why" that I'm looking for is the why X is more important than Y". Notice how for most of those questions, I'm not among the ones repeating those wheels? That's because I've accepted the fact the difference in our opinions cannot be surmounted with logic and reason. I'm not saying that TC39 has been unreasonable, but rather that I can't fathom your sense of reason on some of these issues, hence the questions. When that reason comes down to an unverified, unwarranted impression that's refutable with logic, its outside of my ability to comprehend.

Even if I cannot accept your reasoning, as long as I can comprehend it, that's good enough. For the cases where its just a matter of opinion or perspective, I fail to understand why a side is taken when it would be more prudent to either absorb all cases or neither. For instance, the [[Set]] vs [[Define]] debate. Why not provide syntax for both? Currently that's the reality. Anyone can either set or define a new property at any time. Why promote one over the other knowing that it will lead to misunderstandings and bugs?

I get it. You don't think that is likely to happen. Yet there is plenty of anecdotal evidence that it will. Compound this by a quote from @ljharb: "If it can be done, someone will do it." Follow that with another quote of his: "A non-zero risk is unacceptable". While I admit to taking that last one out of context, the inconsistent way that values are being applied are yet another point of confusion, and the main reason there are so many questions in the first place.

I don't expect you to try and address any of that here. You're working on a document that should have this information, right? My comments here are only meant to help you understand the type of information that can help those who don't find this proposal acceptable. What is needed is the information on why X was preferred over Y, with an understanding of the undesirable consequences of both X and Y. Its a lot to ask, especially since this information wasn't gathered at the time it was being discussed.

Process-wise, if this kind of info were collected for future proposals, it would help not only the community to understand the thoughts going in, but possibly even give TC39 a better perspective on the concerns of the community. It's just a thought.

@littledan
Copy link
Member

TC39 does a lot of reasoning from first principles, but there's no sound first-principles answer to the question of how different goals are weighed. The README and FAQ do their best to explain why the goals are taken as very important, but there's no secret document that I am working on that will convince you that these goals are more important than other goals. Ultimately, we have to make a subjective call, and the way we make the call is based around TC39's committee consensus process that we've been discussing.

What I'd like to do going forward is improve outreach to collect data that can be persuasive to the committee and influence their weighing of goals. I'd like to focus on earlier stage proposals, where the committee members' positions will be easier to influence. This will be a gradual process, and it will only be possible if we can work collaboratively and constructively.

@rdking
Copy link
Author

rdking commented Jan 6, 2019

@littledan Just a suggestion for the future: A common way of making subjective calls in a more deterministic fashion is to create a scoring and classification system. This gives a reasonably well-defined weighing system for goals. It becomes less of a surprise to the community if the weighing system is understood. Even if we cannot agree on purely subjective matters, a scoring system leads to results that are difficult to question.

If you'd like to see such a system in action, we can review this proposals benefits and issues using the scoring system I have for proposals. I even used this system when developing class members.

@littledan
Copy link
Member

littledan commented Jan 6, 2019

Not sure what you mean by scoring and classification. Do you mean listing out the pros and cons, and documenting these well? Or going further and giving numerical weights to those, to make the call? The former seems like a good idea, but I haven't heard about any experience using the latter.

@rdking
Copy link
Author

rdking commented Jan 7, 2019

@littledan I mean numerical weights. For instance, here's why I'm not particularly a fan of this proposal:

The System:

3pts for a logical or technical argument
2pts for a rational or use-case argument
1pt for an emotional or subjective argument

Scoring is positive for features and negative for issues and trade-offs

The Evaluation for Class Fields:

Features

+3: Provides encapsulation
+3: Provides hard private
+3: Provides concise syntax
+3: (I don't agree, but to be fair) Provides syntax for public instance data declaration
+1: Easy to comprehend
Total: 13

Issues

-1: Unpalatable, non-ES-like Syntax
-2: Not Proxy Safe
-2: Interferes with down-stream inheritance
-3: Interferes with upstream inheritance
-2: Introduces avoidable ASI hazards
-2: Introduces context sensitive grammar scenarios with dissimilar meanings
-3: Introduces hard to discover foot-gun if sigil is dropped
-2: Breaks long standing equality between this.<name> and this['<name>']
-3: Depends on Stage 2 proposal to alleviate other known issues not listed here
Total: -20

Overall Score: -7 (Bad Proposal)

There's no way I wouldn't veto this proposal if I was a board member. As you can see, the syntax issue by itself wouldn't have been enough to even bat an eye at. If the list of issues were half as long, this proposal would still have my support. No 1 particular issue is anywhere near enough by itself to warrant stopping. But the collection of issues is just ridiculously damaging from my perspective.

I know you can probably add a few more things to the "features list", but I have other things that I can add to the issue list as well. The point is that I believe if TC39 also used such a system to weigh these proposals, there would be a somewhat surprising change in the "consensus".

@littledan
Copy link
Member

Numerical weights sound game-able and inviting lots of lawyering. Do you have any example of successful use of such a system?

@rdking
Copy link
Author

rdking commented Jan 7, 2019

@littledan Is it game-able? Yes, especially if the conditions for scoring are not deterministic enough or subject to interpretation. For instance, I scored "Interferes with upstream inheritance" as a technical problem but "Interferes with downstream inheritance" as a use-case problem. I very easily could have listed both as either technical or use-case, but no one would expect an upstream inheritance issue, hence technical, while downstream could potentially be explained away with the override concept.

That's why this is where your(TC39's) use of "consensus" would be handy. The consensus would be need to settle on how to score each feature/issue. Once that's out of the way, everything else becomes significantly more deterministic.

As for examples, look into AGILE development. What I've described for you is the process for how tasks are weighed. Everyone first agrees that some simple task is given a particularly low numeric weight. All scoring is then done relative to each developers understanding of the difficulty compared to that baseline. A short discussion leads to a consensus on the weight for that task.

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