tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Make # an object on the instance #75

Closed shannon closed 6 years ago

shannon commented 6 years ago

Update at bottom of description

After reading through the FAQ and issues on the various related repos regarding the sigil I just wanted to run something else by the champions of this proposal.

Please forgive me if this has already been discussed but as I said there are various repos and lots of threads so I may have missed it.

I'm wondering if just making # be a an object property of this makes this proposal more consistent with existing paradigms in Javascript.

For example:

class {
  private x = 'x';

  method() {
    console.log(this.#.x);
  }
}

Would be sugar for:

(function(){
  const __private__ = new Weakmap();
  return class {
    constructor(){
      __private__.set(this, {
        x: 'x'
      });
    }

    method() {
      console.log(__private__.get(this).x);
    }
  }
})();

Why?

Well I get the need for the # sigil for assesor efficiency but there are a couple of points of this proposal that I find unnessarily restricitve.

  1. No variable accessors (e.g. this['#x'] or this[somevar]). This would seem like any use case where you would need this for public properties could also be applied to private properties.
  2. No private property iteration. To me there shouldn't be any reason I shouldn't be able to iterate over private properties without breaking encapsulation. I can think of one use case in particular for a ECS game engine I am working on.
  3. No destructuring. To avoid having to type this.# repeatedly it would be nice to be able to use destructuring at the beginning of a method to access private properties.

With this adjustment all of these should be possible without losing any encapsulation.

Variable accessors:

class {
  private x = 'x';
  private y = 'y';

  method() {
    const prop = condition ? 'x' : 'y';
    this.#[prop] = somecomplexcalc(this.#[prop]) ; 
  }
}

vs

class {
  #x = 'x';
  #y = 'y';

  method() {
    if(condition) {
      this.#x = somecomplexcalc(this.#x);
    } else {
      this.#y = somecomplexcalc(this.#y);
    }
  }
}

Private property iteration:

class {
  private x = 'x';
  private y = 'y';
  private z = 'z';

  method() {
    for(const [prop, value] of Object.entries(this.#)){
        this.#[prop] = somecomplexcalc(value);
    }
  }
}

vs

class {
  #x = 'x';
  #y = 'y';
  #z = 'z';

  method() {
    this.#x = somecomplexcalc(this.#x);
    this.#y = somecomplexcalc(this.#y);
    this.#z = somecomplexcalc(this.#z);
  }
}

Destructuring:

class {
  private x = 'x';
  private y = 'y';
  private z = 'z';

  method() {
    const { x, y, z } = this.#;
  }
}

vs

class {
  #x = 'x';
  #y = 'y';
  #z = 'z';

  method() {
    const x = this.#x;
    const y = this.#y;
    const z = this.#z;
  }
}

Why private in declaration? (I know it's been talked about but I have to try)

First consistency and readability:

class {
  #x = 'x';
  @protected #y = 'y';
  static method (){}
}

vs

class {
  private x = 'x';
  protected y = 'y';
  static method() {};
}

Secondly, with # being a property of this instead of part of the name of the property it makes more sense here.

Conclusion

To me, this makes this proposal seem a little less strange compared to the rest of Javascript. It's a simple mental model. All private properties are stored on this.# which is only accessible from within the class methods.

Keeping the sigil for accessors means it's non breaking for any existing code so I'm ok with it there but I really hope we reconsider using private in the declaration just to keep the syntax consistent and readable.

Edited: typo and broken formatting in github

Updated proposal

I wanted to provide an update to my proposal based on the discussions in the thread. I believe that the # should be moved to before this to make it clearer to developers that #this is a lexically scoped variable.

class {
  private x = 'x';

  method(obj) {
    console.log(#this.x, #obj.x);
  }
}

Would be sugar for:

(function(){
  const __private__ = new Weakmap();
  return class {
    constructor(){
      __private__.set(this, {
        x: 'x'
      });
    }

    method(obj) {
      console.log(__private__.get(this).x, __private__.get(obj).x);
    }
  }
})();

I have also taken the comments of @ljharb and @bakkot into consideration and I have a further addition that will prevent any leakage to mutations.

To avoid any leakage we can make it a syntax error to assign #this to any variable, property, return value, or function parameter. This is actually a really simple syntax check. # must be immediately followed by variable identifier which must be immediately followed by a . or [. With the only exception being for destructuring.

Syntax error example:

const x = #this;
const x = { #this };
const x = { x: #this };
const x = [#this];
somefunction(#this);
return #this;

I'm sure there tons I have missed here but as long as they follow those rules there shouldn't be any leakage.

Valid syntax examples:

const x = #this.x;
#this.method();
#this.x++;
#this['x']++;
const { x } = #this; // exception to the rule but doesn't cause leakage
const x = { ...#this }; //exception to the rule but doesn't cause leakage

This prevents any leakage for mutations.

This doesn't fully address the issue with private keyword implying this.x access but I think it comes pretty close.

Edit: I accidentally left out or [ syntax check that I had in my notes here

shannon commented 6 years ago

I've been thinking more about the #obj reference I have been describing and I realized if you break it out from classes and just think of it as a reference to a private context lexically scoped to the bounds of the containing object you can start to do some interesting things.

The this.#x proposal and my proposal aren't necessarily mutually exclusive (but I admit, there is a bit overlap which may lead to confusion).

//simple non class example
const obj = {
  visit(visitor) {
    #visitor.visited = true; //assign property to private context of visitor within obj
  }
}

//advanced class example, now it gets interesting
const MyObject {
  #visitorNumber = 0;

  visit(visitor) {
    if(!#visitor.visited){
      #visitor.visited = true;
      #visitor.number = ++this.#visitorNumber;

      //other arbitrary MyObject<->visitor context;
      #visitor.x = 'x';
      #visitor.y = 'y';
      #visitor.z = 'z';
    }
  }

  wereTheyAfterMe(them, me) {
    assert(#them.visited, "I don't know them");
    assert(#me.visited, "I don't know you");
    return #them.number > #me.number;
  }

  wereTheyBeforeMe(them, me){
    assert(#them.visited, "I don't know them");
    assert(#me.visited, "I don't know you");
    return #them.number < #me.number;
  }
}

Would be sugar for something like:

class PrivateContext {
  //a special prototype that could be extended in the future,
  assign(obj){
    //just an example to allow Object.assign like functionality since this would be prevented by syntax restrictions
    // e.g Object.assign(#this, { ... }); throws a syntax error;
    return Object.assign(this, obj);
  }
}

class PrivateContextMap extends WeakMap {
  get(key) {
    //create context on first reference
    return super.get(key) || super.set(key, new PrivateContext()).get(key);
  }
}

const obj = (function(){
  const __private__ = new PrivateContextMap();
  return {
    visit(visitor) {
      __private__.get(visitor).visited = true;
    }
  }
})();

const MyObject = (function(){
  const __private__ = new PrivateContextMap();
  return class MyObject {
    #visitorNumber = 0; //I didn't transpile this since I assume it will be handled based on the current proposal
    visit(visitor) {
      const __visitor__ = __private__.get(visitor);
      if(!__visitor__.visited){
        __visitor__.visited = true;
        __visitor__.number = ++this.#visitorNumber;
        __visitor__.x = 'x';
        __visitor__.y = 'y';
        __visitor__.z = 'z';
      }
    }

    wereTheyAfterMe(them, me) {
      const __them__ = __private__.get(them);
      const __me__ = __private__.get(me);
      assert(__them__.visited, "I don't know them");
      assert(__me__.visited, "I don't know you");
      return __them__.number > __me__.number;
    }

    wereTheyBeforeMe(them, me) {
      const __them__ = __private__.get(them);
      const __me__ = __private__.get(me);
      assert(__them__.visited, "I don't know them");
      assert(__me__.visited, "I don't know you");
      return __them__.number < __me__.number;
    }
  }
})();

Now of course you could create a private context weak map but there are a few problems with this approach:

  1. That weak map would not be lexically scoped and you risk accidentally leaking the weak map itself which means you leak context for all objects that have visited.
  2. You have to keep referencing back to the context weakmap across methods so lots of this.#visitorContext.get(x)
  3. You would have to deal with the context create on first reference yourself or import a PrivateContextMap type object to use.
class MyObject {
  #visitorContext = new WeakMap();
  visit(visitor) {
    const visitorCtx = this.#visitorContext.get(visitor) || this.#visitorContext.set(visitor, {}).get(visitor);
    if(!visitorCtx.visited) {
      visitorCtx.visited = true;
      visitorCtx.number = true;
      visitorCtx.x = 'x';
      visitorCtx.y = 'y';
      visitorCtx.z = 'z';
    }
  }

  wereTheyAfterMe(them, me) {
    const themCtx = this.#visitorContext.get(them);
    const meCtx = this.#visitorContext.get(me);

    assert(themCtx, "I don't know them");
    assert(meCtx, "I don't know you");
    return themCtx.number > meCtx.number;
  }

  wereTheyBeforeMe(them, me) {
    const themCtx = this.#visitorContext.get(them);
    const meCtx = this.#visitorContext.get(me);

    assert(themCtx, "I don't know them");
    assert(meCtx, "I don't know you");
    return themCtx.number < meCtx.number;
  }
}

The next bit may be controversial if this.#x goes through but the private keyword in a class could just be declaritive shorthand for assigning to the private context of the instance.

class MyObject {
  private x = 'x';

  private method() {
    return #this.x;
  }
}

becomes

class MyObject {
  constructor() {
    #this.x = 'x';
    #this.method = () => #this.x;
  }
}

I could see this being very useful for game engine pipeline since objects can often times pass through multiple stages of the pipeline and I have to create/access lots of WeakMaps to manage context. This weakmap context approach is crucial because it drastically reduces the risk of creating unintentional side effects within the stages of the pipeline and it allows assets to be garbage collected.

MichaelTheriot commented 6 years ago

Just passing through here but to me #this is much more intuitive and easier to comprehend than this.#.

this.# suggests some property exists but is hidden somehow on the 'this' object.

this clearly conveys that it is another reference entirely that is only accessible within the declaration.

Not to mention the visual ease and ability to access variables via bracket notation. #this is simply more true to what is actually going on behind the scenes with the weak map. I hope #this can gain some traction.

ljharb commented 6 years ago

@MichaelTheriot you may want to read the FAQ; it is indeed a hidden property on the this object - you can also do obj.#foo and access the private field on another object, as long as that object is an instance of the same class.

MichaelTheriot commented 6 years ago

Sorry for misunderstanding, I was under the impression this was syntactic sugar for the WeakMap implementation of private properties. There is no way to add a "hidden property" onto objects in the language today, so if what you are saying is true then this is indeed not a proposal for syntactic sugar but an entirely new feature incompatible with JavaScript engines today?

Given that the front page of this proposal makes no suggestion otherwise, and that this is a practice in use today since the introduction of WeakMap I hope you can forgive me for thinking so.

I read the FAQ and the pitfall with mallory is easily resolved by binding functions to the this object when adding them to their private WeakMap reference. You can also use WeakMaps to access private fields on other objects of the same class, akin to obj#foo. I'm sure someone has investigated this thoroughly and the answer is out there but at the moment syntactic sugar for WeakMap seems like the perfect way to implement this at least to me. Do you have any other examples to discount WeakMap, or can direct me to a forum where this has been discussed?

bakkot commented 6 years ago

@MichaelTheriot, a WeakMap is semantically equivalent to a hidden property. I do not expect implementations to use WeakMaps under the hood, however. I suspect most users will think of this feature as providing private properties rather than sugar on WeakMaps, and would encourage that interpretation; users should not have to think about possible desugarings, especially those which don't map to what engines are actually like to do.

ljharb commented 6 years ago

@MichaelTheriot it is indeed (conceptually) sugar for WeakMaps; you can put any this into the WeakMap upon construction, and then if you had, say, equals(obj) { return this.#secret === obj.#secret }, it would work fine. This isn't an "entirely new feature" in the sense that you can already do this with WeakMaps; only in the sense that it's ergonomic and straightforward to use.

shannon commented 6 years ago

@ljharb I assume you mean this.#secret === obj.#secret and not this#secret === obj#secret or has this changed? This is explicitly called out in the FAQ as an ASI hazard. I would much prefer this#secret but as we have been repeatedly directing users to the FAQ for any issues regarding the sigil I don't want to be adding to the confusion here. You last two comments have included this syntax though.

ljharb commented 6 years ago

@shannon yes, my mistake, just typing too quickly :-) I've updated both of the comments you mentioned.

MichaelTheriot commented 6 years ago

@bakkot I don't expect engines to use a WeakMap under the hood, but if we agree it's semantically equivalent we should take advantage of it. Can we preserve what this offers? You can access private properties via bracket notation using a semantically equivalent WeakMap as such:

const Person = (() => {
  const __private__ = new WeakMap();

  return class {
    constructor(name, ssn = null, cc = null) {
      this.name = name;
      __private__.set(this, {ssn, cc});
    }

    get hasSSN () {
      return !!__private__.get(this).ssn;
    }

    get hasCC () {
      return !!__private__.get(this).cc;
    }

    hasSecret (key) {
      return !!__private__.get(this)[key];
    }
  };
})();

const joe = new Person('joe', '123');
joe.hasSSN; // true
joe.hasCC; // false
joe.hasSecret('ssn'); // true
joe.hasSecret('cc'); // false

and I would hope the equivalent would be

const Person = class {
  constructor(name, ssn = null, cc = null) {
    this.name = name;
    #this.ssn = ssn;
    #this.cc = cc;
  }

  get hasSSN () {
    return !!#this.ssn;
  }

  get hasCC () {
    return !!#this.cc;
  }

  hasSecret (key) {
    return !!#this[key];
  }
};

const joe = new Person('joe', '123');
joe.hasSSN; // true
joe.hasCC; // false
joe.hasSecret('ssn'); // true
joe.hasSecret('cc'); // false

I only favor this because it is intuitive to me. This is confusing:

All of these work with #this notation and I do not see a drawback of simply prefixing the #. Are these are being left out because of syntax or a perspective on how private properties should behave?

I expected private properties to behave identical to regular properties, just with their reference private to the class internals. What I'm coming away with is something else. This seems more like statically typed variables that can be accessed by <instance>.# syntax. That's all. It certainly does not seem like this.#a is a hidden property on the this object, just that it is masquerading as one and leading to a lot of confusion when the reality sets in that it isn't.

I would not expect private properties to be statically typed after a decade of experience, and it feels very inconsistent given that this is not a statically typed language. If this is the intent then private "properties" is misleading in this context and I would suggest a rename at least to convey something more accurate to what these actually are.

Sorry if I'm late to the show and what I'm saying is less valuable now, but I have to think there are merits here that need consideration given the small cognitive burden (#this.a instead of this.#a). It'd be nice to gauge interest between the two. I hope the questions I'm raising help get a better feel to what the reception will be.

bakkot commented 6 years ago

@MichaelTheriot

if we agree it's semantically equivalent we should take advantage of it

... Why? It's not always the case that more powerful is better.

Are these are being left out because of syntax or a perspective on how private properties should behave?

The latter. There's some more discussion here and here, though the major thing for me is this:

JavaScript currently has exactly one kind of strong privacy, and that is the kind offered by closures. The privacy model of this proposal is pretty much identical to the privacy you get there (i.e., you have access to this.#x anywhere where the #x name is lexically in scope), and I think that's a good thing. I would not want to have to teach several different radically different models. Given this intuition or understanding, the answers to your questions are immediate, I think.

If I were to think of this as analogous to WeakMaps, I would want to think of it as having one closed over WeakMap per private field, not a single WeakMap storing a "private object".

It certainly does not seem like this.#a is a hidden property on the this object, just that it is masquerading as one and leading to a lot of confusion when the reality sets in that it isn't.

Can I ask why not? It's certainly intended to: for example, this.#foo() will call the resulting function with this as the receiver, while (0, this.#foo)() will get undefined; values are associated with individual objects rather than there being a single value shared across all objects; etc. They are not dynamic, but they're still very much properties, to me.

I would suggest a rename at least to convey something more accurate to what these actually are.

There's some discussion of this over here, but personally I don't expect it to matter what we choose to call this. People are going to think of it as private fields no matter what we do. And I don't think that's a bad thing, because it seems like it tends to produce pretty much the right mental model.

given the small cognitive burden (#this.a instead of this.#a)

I don't want to focus too much on the details of this alternate syntax proposal, though I think I linked upthread to some of the previous times it's come up if you really want to read more. Briefly, though, I dispute that this burden is as small as you think: for example, does #a.b.c map, in this proposal, to a.#b.c or a.b.#c? I claim neither is obviously correct, which is a bad thing.

I have to think there are merits here that need consideration given the small cognitive burden

They are definitely worth consideration! That said, we've considered all the things brought up in this thread a fair bit already, I think.

shannon commented 6 years ago

@bakkot

Briefly, though, I dispute that this burden is as small as you think: for example, does #a.b.c map, in this proposal, to a.#b.c or a.b.#c? I claim neither is obviously correct, which is a bad thing.

Since '#' would be an operator, I think it's pretty obvious that it's (#a).b.c. So it would be comparable to a.#b.c in the current proposal. Access c from a private object b on a. It's really only confusing when you mix the two syntax. I'm not sure how it could be confused with a.b.#c though, so I'm not sure how you found it ambiguous.

ljharb commented 6 years ago

@shannon the proposed :: operator has ::a.b.c as sugar for a.b::c; which would be similar to the a.b.#c option.

shannon commented 6 years ago

@ljharb Do any other operators behave this way? :: is a stage 0 proposal at this point so I would not consider that to be normal to JS and should not be the cause of confusion when comparing to the rest of JS.

shannon commented 6 years ago

@ljharb In either case if that behavior is desired if that proposal is considering it then as long as it is well defined then the ambiguity should be gone. But to say it's confusing here is to say it's confusing for ::.

bakkot commented 6 years ago

@shannon I'm not aware of any operators which behave as you're proposing # would behave. In any case, I think the important point is not that it would be different from ::, which as you say is not at all part of the language, but just that the proposed behavior of :: shows that at least some people have an intuition that a prefix-placed operator which modified member access would bind less tightly than the member access, while you have the intuition it would bind more tightly: hence it is not obviously correct to do one or the other.

Yes, we can pick one and make everyone learn yet another precedence rule, but that's still a cause of cognitive burden which is absent with the current proposal, which is all I meant to claim.

shannon commented 6 years ago

@bakkot Fair enough, I didn't think it all the way through. Now that I've looked at it again and you're right it should be the way you have described. x + a.b.c would be x + c. So my proposal would have to follow suit with the :: operator and I'm ok with this.

However, I think this is a small price to pay to reduce the cognitive burden in other ways though.

MichaelTheriot commented 6 years ago

@bakkot

Can I ask why not? It's certainly intended to: for example, this.#foo() will call the resulting function with this as the receiver, while (0, this.#foo)() will get undefined; values are associated with individual objects rather than there being a single value shared across all objects; etc. They are not dynamic, but they're still very much properties, to me.

Properties are much more than just plain variables and bounded functions.

I do not agree that these can be "properties" if they are not dynamic nor support the above behavior; this is fundamental to properties in JavaScript. At most, these share the access syntax and bind functions to the receiver, but otherwise are just variables.

There's some discussion of this over here, but personally I don't expect it to matter what we choose to call this. People are going to think of it as private fields no matter what we do. And I don't think that's a bad thing, because it seems like it tends to produce pretty much the right mental model.

Thanks for the link; I was not aware of this proposal. To me, this proposal gets closer to the mark with "instance variables" because that is what these effectively are. The mental model is simpler when we stop calling these properties because we lose the expectations that carries (at least, I do).

I think it would make sense to interpret these as private properties in the context of a static language, since these generally do not include the extra behaviors of JS properties, but this is a dynamic language, properties here do exhibit this behavior, and this is unnecessary confusion that should be avoided.

does #a.b.c map, in this proposal, to a.#b.c or a.b.#c? I claim neither is obviously correct, which is a bad thing.

I would at a naive glance say it is obvious (#a).b.c maps to a.#b.c, and don't see how this would be confusing at least in the context of how the language is defined today.

I suppose only the terminology is an itch at this point. It might seem trivial, but I anticipate others will have similar expectations and slip ups when they hear private properties landed. I have however gotten a better grasp of what this proposal aims for and appreciate the discussion!

bakkot commented 6 years ago

@MichaelTheriot, thanks for expanding.

To me, those are (sometimes) things that it happens you can do with properties, but I don't really think of them as being fundamental to what properties are, and I don't think most other programmers do either. For what it's worth:

I cannot delete the property, delete this.#foo

Not all public properties can be deleted.

I cannot test the property exists, #foo in this

True, though part of the point of the more static shape offered by this proposal is that you shouldn't generally have to.

I cannot access this.#foo by a variable key, this[#variableKey]

True.

I cannot assign this.#foo to another object via Object.assign()

Not all public properties are copied by Object.assign.

I cannot get a property descriptor for this.#foo, Object.getOwnPropertyDescriptor()

True.

I cannot declare a getter or a setter private property (AFAICT)

I expect you will be able to; see this proposal.

I cannot use the same set of valid property names nor even use symbols:

Not really sure what this means.


I also want to note that for in also only gives a subset of properties, and with only introduces a subset of properties into the current scope. All four of these subsets - those seen by for in, those used by with, those which are delete-able, those which are copied by Object.assign - are distinct, and none are the full set. If things which do not have all of the above characteristics are not properties, we have at least five distinct categories of thing going by that name already. (Though you don't mention it, .length on arrays is another kind of property still, being a property which claims per getOwnPropertyDescriptor to be a regular data property but such that changing it has side effects.)

As such, I really don't think that most programmers tend to think of "properties" as things which have all of the above characteristics (and if they do that's a problem in itself); this just seems like a list of characteristics which (some) properties happen to have.

For me the most important characteristic of a "property" is that it is associated one-to-one with a specific object, rather than (as with variables) with a specific scope, and secondarily that invoking a function via a property access binds the function's this (unless it is already bound). These seem much more fundamental to me than how they're treated by Object.assign. This makes #fields seem much more like properties than variables, to me.


I anticipate others will have similar expectations and slip ups when they hear private properties landed. I have however gotten a better grasp of what this proposal aims for and appreciate the discussion!

Some of that seems inevitable, unfortunately. We'd like to do our best to minimize it, of course; I think the current proposal does the best job that reasonably can be done with that without making the feature much less good. Anyway, happy to chat about the design decisions here (and to continue to do so), and I'm glad I could help explain them a bit.

MichaelTheriot commented 6 years ago

@bakkot

To me, those are (sometimes) things that it happens you can do with properties, but I don't really think of them as being fundamental to what properties are, and I don't think most other programmers do either.

Most programmers probably do not; these behaviors altogether are pretty unique specifically to this language. Here are behaviors for properties generally not present in most languages (altogether):

Calling something that does not fit this profile a property raises flags to me. If we ignore these and think of JS like a static language, sure, but it simply is not and I anticipate among newcomers and experienced developers alike this will be more confusing than it needs to be.

Not all public properties can be deleted. Not all public properties are copied by Object.assign.

True, but deleting a public property is never a syntax error like it would be for these. Any mishap in deletion or properties missing in iteration are run-time behaviors explicitly configured by the programmer.

Not really sure what this means.

By my examples I meant to convey:

Every restriction I am listing is actually already present on variables.


Though you don't mention it, .length on arrays is another kind of property still, being a property which claims per getOwnPropertyDescriptor to be a regular data property but such that changing it has side effects.

That is a quirk that I definitely consider a missed opportunity to catch when Object.getOwnPropertyDescriptor() landed. For what it's worth you can at least implement an Array yourself that exhibits this behavior by using a Proxy. 😕

bakkot commented 6 years ago

Here are behaviors for properties generally not present in most languages (altogether) [...] Calling something that does not fit this profile a property raises flags to me.

Wait, where does this intuition come from? I have exactly the opposite intuition: the characteristics which are a priori most core to being a "property" are those which are the most in common with "properties" in other languages, whereas details like delete semantics are just JavaScript-specific extensions to what you can do with its properties. That in JavaScript you happen to be able to (for example) use delete with at worst a runtime error for all the kinds of properties currently in the language doesn't really imply that this is a necessary characteristic of anything we might reasonably call a property. It just... happens to be the case currently.

True, but deleting a public property is never a syntax error like it would be for these.

Sure, because you don't have enough information to know ahead of time that the property in question is not configurable. Here you do, and we usually feel it's better to get errors as early as possible.

This does not feel, to me, like it has anything to do with whether it is "a property". It's just a property which the language is able to make more guarantees about than usual.

Any mishap in deletion or properties missing in iteration are run-time behaviors explicitly configured by the programmer.

I don't really buy "explicitly configured by the programmer", especially for things like unscopables, nor why it would matter. The point is just that not all properties have these characteristics: programmers already have the power to make a property which is invisible to for in, for example. That there is now another kind of property with these characteristics which some properties already had doesn't seem like it should be that much of a big deal.

MichaelTheriot commented 6 years ago

details like delete semantics are just JavaScript-specific extensions

These JS-specific "extensions" are core to the language. In JS, "static-like" properties are an extension. Naturally, something claiming to be a property but being largely inconsistent with properties as already defined in the language comes off confusing.

Sure, because you don't have enough information to know ahead of time that the property in question is not configurable. Here you do, and we usually feel it's better to get errors as early as possible.

I'm an outsider here; I do not know what to say to "we usually feel..." or what weight it carries. All I can say is this is a duck-typed language that is constantly guarding against run-time errors, and this would be an exception to the rule. For what it's worth an avenue does exist to check if a property can be deleted at run-time.

That there is now another kind of property with these characteristics which some properties already had doesn't seem like it should be that much of a big deal.

I think it is potentially a big deal to call these private properties, give it a syntax similar to properties, and have no property operators, bracket notation, or descriptor methods work here. The next question is naturally, why is the rest of JS not like this? Why this oddball in an otherwise dynamic language?

bakkot commented 6 years ago

These JS-specific "extensions" are core to the language. In JS, "static-like" properties are an extension. Naturally, something claiming to be a property but being largely inconsistent with properties as already defined in the language comes off confusing.

I really do not agree, but I doubt we're going to be able to convince each other here. They are inconsistent in a few ways, but there are already many inconsistencies in how various things called "property" work, and people seem to be able to work with that just fine.

All I can say is this is a duck-typed language that is constantly guarding against run-time errors, and this would be an exception to the rule.

There are dozens of other early errors in the language. For example, delete x is a syntax error in strict mode (even though it sometimes has effects in sloppy mode), calling super outside a constructor is a syntax error, importing a name which does not exist is a syntax error, 0++ is a syntax error, etc.

Any of those could have been runtime errors, but because there is enough information to statically detect them they're instead syntax errors. I don't think this rule you refer to exists.

I think it is potentially a big deal to call these private properties, give it a syntax similar to properties, and have no property operators, bracket notation, or descriptor methods work here.

Why is it any more of a big deal than to have properties which don't show up in for in, which are not copied by Object.assign, or which with does not introduce as bindings?

trusktr commented 6 years ago

existing paradigms in Javascript.

Yes, please, let's not forget this!

If we're in the scope of the private or protected instance, we should be able to use any existing language features, f.e. for..in or Object.keys.

Personally, i like the prefix notation, f.e.

this.foo
@this.bar // protected
#this.baz // private

because, then

Object.keys(#this)
for ( const key in #this ) {}
const { bar } = @this

which is similar to my inheritance implementation, lowclass, where you can do

this.foo
protected(this).bar
private(this).baz

Object.keys( private(this) )
for ( const key in private(this) ) {}
const { bar } = protected(this)

We should be able to do literally anything we can do nowadays with public properties.

More examples, using notation:

const key = makeSomeString()
#this[ key ] = 'foo'

The same with Lowclass:

const key = makeSomeString() // for example, a UUID, or literally anything
private(this)[ key ] = 'foo'

With notation:

console.assert( 'bar' in @this )

With Lowclass:

console.assert( 'bar' in protected(this) )

At bare minimum, let's let these new features work just like public does, even if they work separately, where by "separately" I mean we can't use for..in on all of them at once, f.e.:

for ( const key in this ) {} // only public keys

// we have to do it on them separately:
for ( const key in #this ) {} // private keys
for ( const key in @this ) {} // protected keys

With Lowclass:

for ( const key in this ) {} // only public keys

// we have to do it on them separately:
for ( const key in private(this) ) {} // private keys
for ( const key in protected(this) ) {} // protected keys

Let's let people have the same flexibility they do today with public stuff.

In Lowclass, we can also leak protected or private scope purposefully if we want:

// FooBar.js

let fooPrivate

const Foo = Class((public, protected, private) => {
  fooPrivate = private

  private.foo = "foo"
})

const Bar = Foo.subclass((public, protected, private) => ({
  test() {
    console.log(fooPrivate(this).foo) // "foo"
    console.log(private(this).foo) // "bar"
  },
  private: {
    foo: "bar"
  }
}))

export { Foo, Bar }
import { Bar } from './FooBar.js'

const bar = new Bar
bar.test()

// output:
foo
bar

That example is here in the lowclass tests.

The following is just a stretch, but maybe it is possible to have even that previous flexibility with the builtin feature:

class Foo {
  private as fooPrivate // makes a variable fooPrivate in the same scope as the Foo variable
  private foo = "foo"
}

class Bar extends Foo {
  private foo = "bar"

  test() {
    console.assert( fooPrivate(this).foo === 'foo' )
    console.assert( #this.foo === 'bar' )
  }
}

const bar = new Bar
bar.test()

or even

let fooPrivate

class Foo {
  private foo = "foo"

  constructor() {
    fooPrivate = #this // expose specific private instance
    fooPrivate = private(this) // lowclass version
  }
}

new Foo
console.assert( fooPrivate === 'foo' )

It would not be possible to do the following:

let fooPrivate
Foo.prototype.hack = function() { fooPrivate = #this }
bar.hack() // maybe throws an error
console.log( fooPrivate ) // or undefined

in lowclass:

let fooPrivate
Foo.prototype.hack = function() { fooPrivate = private(this) }
bar.hack() // error, private is undefined

In lowclass, protected(this) returns the protected prototype chain. private(this) returns the private prototype chain, but it is much different than the protected prototype chain.

this // leaf of the public prototype chain
protected(this) // leaf of the protected prototype chain
private(this) // leaf of the private prototype chain

Any given instance has exactly 1 public prototype chain and 1 protected chain, but has as many private chains as there are number of classes in the instance's inheritance hierarchy.

For example, suppose we have this class hierarchy: Baz extends Bar extends Foo, then this is what the prototypes for an instance of Baz looks like:

Public prototype chain, the `this` we all know (the public object returned from
`new Baz`):

     +------+          +-----+          +-----+          +-----+
     | this |<---------| Baz |<---------| Bar |<---------| Foo |
     +------+          +-----+          +-----+          +-----+

Protected prototype chain, returned from `protected(this)` call in the code of
any class in the hierarchy (Foo, Bar, Baz):

     +-----------------+          +-----+          +-----+          +-----+
     | protected(this) |<---------| Baz |<---------| Bar |<---------| Foo |
     +-----------------+          +-----+          +-----+          +-----+

There are multiple private prototype chains, one per class. The call to `private(this)`
in the code of a given class returns the two-object-long private chain
specifically associated with that class:

     +-----------------+          +-----+
     | private(this)   |<---------| Baz |   // this one is returned if private(this) is called inside Baz code
     +-----------------+          +-----+

     +-----------------+          +-----+
     | private(this)   |<---------| Bar |   // this one is returned if private(this) is called inside Bar code
     +-----------------+          +-----+

     +-----------------+          +-----+
     | private(this)   |<---------| Foo |   // this one is returned if private(this) is called inside Foo code
     +-----------------+          +-----+

In the public prototype chain, Baz, Bar, and Foo in the diagram are the prototypes we all know, accessible on Baz.prototype, Bar.prototype, and Foo.prototype, respectively.

In the public chain of the instance, Baz, Bar, and Foo contain the protected properties and methods that were defined in their respective lowclass Class definitions.

In the private prototype chains, Baz, Bar, and Foo contain the private properties and methods that were defined in the respective class definitions, but instead of connecting them all together in one long chain, a new private instance is made for each prototype associated with each class (instead of just a single instance at the front of the chain like with protected).

Just throwing the idea out there to show what's possible and to show how (conceptually, not literally) it may be possible to "desugar" protected and private sigils to this multi-prototype form.

A current downside of lowclass is that protected(this) returns the leaf of the chain, so it is possible that someone can take advantage of that to modify the prototype of all instances in the entire application that inherit from some base class. This should not be possible with the builtin implementation.

trusktr commented 6 years ago

~In my above example, maybe & doesn't work well because it is a bitwise operator. Just replace it with some other symbol.~

Updated the above to use @. Personally, because I think protected will be more commonly used than private, maybe give protected the better symbol and private the at-sign:

#this.bar // protected
@this.baz // private

The & symbol is nicer than @ though. Could it be used? Not sure if it has any conflicts with the bitwise operator in some case.

Or maybe just

#this.bar // protected
##this.baz // private
// hmm, I think I like this.

With prefix form, the . operator isn't so awkward, making #this seem like the "private version of this", then accessing some property like normal. In fact, many devs I know say "this dot that", and placing the # after the . gets in the way of that concept.

seansd-zz commented 6 years ago

I'm super excited just to have class fields . . . but with that said (yeah poo sandwich i know ;-) ). . .

My biggest problem with the field proposal (especially for private), is in 'class' syntax being forced to use the 'this' key word which is completely unsafe. . . as an example:

'use strict';

class Point2D {

    #x = 0;
    #y = 0;
    #_str = '';

    constructor(x, y) {

        this.#x = x;
        this.#y = y;  // here this is safe to use b/c we know it has to be a 'new' object
    }

    get x() { 
        return this.#x; 
    }

    set x(v) {
        if (typeof v === 'number') {
            this.#x = v;
        }
        return false;
    }

   toString() { 
       return this.#x + ', ' + this.#y;
   }
    /* in both the getter/setter methods and toString method above, 
     * there is NO GUARANTEE that 'this' is defined
     * or that 'this' is an instance of Point2D
    */

    /// . . . 

}

I'd be fine with requiring '#x', but not 'this.#x'. The previous sytnax would basically for the engine to assume an implied 'this' pointer / object and do the validation for the developer. . it could thrown an error automatically for us when 'this' isn't defined or isn't a direct instance of an given class. . . othewise as developers in order to ensure safety we'd have to do that kind of check ourselves in EVERY single method call. . .which is super cumbersome and painful. . .

bakkot commented 6 years ago

@seansd, the currently specified semantics are that if the engine executes obj.#x and finds that obj does not have #x - that is, obj was not produced by the class - then a TypeError is thrown. You don't have to do that check yourself.

littledan commented 6 years ago

OK, seems like @shannon and I agreed in https://github.com/tc39/proposal-class-fields/issues/75#issuecomment-360881682 that it would be fine to pursue this as a follow-on proposal. At the same time, I'd like to keep the current class fields proposal minimal; it's already at Stage 3, so it's somewhat late to go adding additional features. See CONTRIBUTING.md for some details on how to get started. Closing this issue, but feel free to follow up here for help getting started.