rdking / proposal-class-members

https://zenparsing.github.io/js-classes-1.1/
7 stars 0 forks source link

I need to replace `let/const`. Any suggestions? #12

Closed rdking closed 5 years ago

rdking commented 5 years ago

Here's the problem. I'm re-writing the README in terms of the new suggestion in #10. This means that instead of having "instance-closure variables", I now have "instance-private properties". This means let and const feel out of place since these terms define variables instead of properties. I already know that, even though I desperately want to, I can't use private since it will get shot down over the insanely weak "but I expect private x to be accessed with this.x" argument. Even PHP didn't uphold that expectation...

Anyway, I'm open to suggestions. If it weren't for the fact that Symbols can be property names, I would have kept let and const. However, who wants to deal with developers thinking that because they can write let [someSymbol] = 2 in a class definition, that it's ok to do that in a function? I know I don't.

rdking commented 5 years ago

For now in the documentation, the keyword will be priv. It's close enough to private without carrying all the legacy implications and Typescript stumbling blocks.

Igmat commented 5 years ago

What about this?

class A {
     private [somePrivate] = 2;

    someMethod() {
        assert(this[somePrivate] === 2);
        assert(typeof somePrivate === 'symbol');
    }
}
rdking commented 5 years ago

Like I said before, I'd love to, but I don't want to deal with the push-back over Typescript's use of private and the pointless expectation that private x implies this.x.

hax commented 5 years ago

This means let and const feel out of place since these terms define variables instead of properties.

I don't think this is a very important issue to most programmers. If field is an accept term, then variable will be very ok even it's technically a property. Some other programmers just use var for both local vars and class properties.

So I think let/const still a valid choices (without the private x implies this.x issue even I also agree it's weak).

I think let [someSymbol] = 2 syntax (or priv [someSymbol] = 2) has a big issue (just like current field proposal), syntax ambiguity with destructuring. This is much worse than "private x implies this.x". We'd better still use let x = 2 and desugar it to auto generated symbol for ergonomic.

You already know I have some other keyword candidate to map private/protected/public, but I think priv x = 2 is also ok, if you use both priv/pub. If you only want keyword for private I think internal is also a good candidate (though 8 letters seems too long for ergonomic).

mbrowne commented 5 years ago

I also vote for keeping let/const regardless of implementation details.

ljharb commented 5 years ago

imo let/const can’t possibly make sense if you’re accessing them on an object - ie, if you have per-instance data, and you need to access them on multiple objects at the same time.

hax commented 5 years ago

@ljharb Dart, Scala, Swift, Groovy, Kotlin... All new languages use same keyword for both local vars and instance vars. I don't understand why JS can't.

mbrowne commented 5 years ago

I should qualify my comment: it only applies in the context of this proposal. More generally, this is one of the reasons I prefer the class fields proposal: field/property declarations look like field/property declarations. But without a prefix like #, I think let/const declarations would be the next best thing.

rdking commented 5 years ago

@hax

I don't understand why JS can't.

The problem is that ES already has a history of not doing things that way. There is absolutely no way to declare a property using var/let/const. On top of that, you don't want to conflate 2 mental models that have a long history of being kept separated. Variables and properties are such things.


As for priv [x] = 'foo';. I dropped that syntax. I couldn't rationalize the reason why I wanted it against the costs of having it. So it's gone. The README has more updates.

mbrowne commented 5 years ago

IMO using priv instead of let/const significantly worsens this proposal. Although you seem to insist on dismissing this point as invalid, the fact is that some developers (the majority, I suspect) will expect private x to be accompanied by this.x for access. And I don't think the expectation would be different for an abbreviation like priv. One of the reasons I have considered this proposal a potentially viable alternative to class fields is the let/const syntax, which doesn't come with that baggage (although of course it comes with a lot of other baggage). (Same with classes 1.1 and the var syntax, but I prefer let/const.)

mbrowne commented 5 years ago

Although I don't like this idea either, just to throw it out there, I think local would still be better than priv, if you insist on getting rid of let/const.

rdking commented 5 years ago

@mbrowne I'm not insisting on dismissing your point. I just need a stronger argument to get me past the conflation of variables and properties. Your argument about other languages doesn't work for me because those other languages have had that conflation in them from the beginning, whereas ES has kept them separate since the beginning. I need an argument that says they can be conflated without breaking the mental model differences between them in ES. Otherwise I just can't do let/const.

If you can give me a strong-enough argument, I'll be happy to reconsider.

hax commented 5 years ago

@rdking I understand that why you think let/const should not be used for property. I can't give you a "strong-enough" argument, I only can give you a relative strong argument..

I think long history make nothing more valid, especially every one to two years the js community are doubled, so it's not a really "old" language if you consider the community. There are also many (don't know the accurate number) js programmers also use other languages, especially Dart, Swift, Kotlin which are popular languages for client-side developing. So I think at least fullstacks engineers and those who do both web developing and client developing, have no issue about the conflation.

On the other side, if you consider current field proposal ,which break too many current js syntax/semantic, you can believe the conflation cost is ok for get rid of other much huge cost! 😂

ljharb commented 5 years ago

We’re designing for all future JS devs, the majority of whom will likely never have used another language. It’s helpful to take lessons from other languages, but JS is its own thing, and it’s ok if we deviate from other languages in order to achieve a more consistent mental model.

hax commented 5 years ago

it’s ok if we deviate from other languages in order to achieve a more consistent mental model.

Agree, but before that you should not break current mental model of JS. I will say most programmers will agree current proposal fail on that as the feedbacks we got.

rdking commented 5 years ago

I think I just came up with a way to justify continuing to use let/const.

@ljharb I need you to give what I'm about to suggest some real thought. I know you mentioned that there's a seeming embargo on shorthand notations, but in this case, it may turn out to be a saving grace. With this idea, I even fixed the imbalance problem that led me to give up on shorthand notation in the first place.


Facts:

Solution:

Require all members to use the same namespace, but only within the class definition.

Basically, this would mean it would be impossible to declare a public and private member with the same name. However, it would be perfectly fine to later attach a new public member that happens to have the same name as an existing private member.

Advantages:

Example

//Throws due to duplicate name in class definition
class Bad {
  let x = 0;
  get x() { return x; } //Syntactically ok, but weird
}

class Good {
  let _x = 0;
  let y = 1;
  get x() { return _x; } //Because we can't duplicate names
  print() { console.log(`y = ${y}, x = ${x}`);
}

var g = new Good();
g.print(); //y = 1, x = 0
g.y; //undefined
g.y = 3; //This is ok because private members are still in their own namespace
g.print(); //y = 1, x = 0
g.y; //3
ljharb commented 5 years ago

What about let ['x'] = 0; or let [var] = 0 in the first example? Would it throw at construction time, or at class evaluation time? (or would that syntax not be allowed, to keep it static?)

What happens if the class does Object.keys(this).map(k => this[k]) after g.y = 3. Would it include the y? What if it did return this.y despite not having any y declared anywhere? Would it return undefined and then later 3, or would it throw because there's a let y?

rdking commented 5 years ago

@ljharb That's a no-go. I removed that possibility earlier. It would only be good for creating Symbol-keyed private properties, but since private property names are already non-enumerable, I couldn't conceive of a reason to need Symbol names for private members. I'm still open to arguments on that.

What happens if the class does Object.keys(this).map(k => this[k]) after g.y = 3. Would it include the y?

y is private, and therefore not enumerable.

What if it did return this.y despite not having any y declared anywhere? Would it return undefined and then later 3, or would it throw because there's a let y?

I'm not sure I understand this question. If you're asking what would happen if there was a function in the class that ran return this.y, then yes, the first g.y would return undefined and the second would return 3. When you don't use shorthand notation, you get exactly what you asked for.

ljharb commented 5 years ago

i'm talking about the public y property - it'd have to show up in Object.keys, so what happens when they try to access it? If you're saying that this.y always is the public property, then great, that all makes sense!

Given that, what's the benefit to denying class authohrs the ability to do y() {} or get y() {} when they have a let y?

rdking commented 5 years ago

Oh. OK. this.y is enumerable, so before being set, it wouldn't exist to be enumerated. After being set it would exist as enumerable. So after g.y = 3, the public y would be included in the map.

rdking commented 5 years ago

The thing to remember is that the rule about keeping public and private members within the same namespace only applies within the lexical scope of the definition during the evaluation of that definition. Even the constructor of the class is free to add new public properties that share the name of private ones.

hax commented 5 years ago

@ljharb

Consider current js allow class X { x() {}; get x() {} }, it seems we could also allow class X { let x; get x() {} } and leave the duplicate naming issue to linters. Anyway, I think it's a relative small bikeshed issue.

rdking commented 5 years ago

@hax Nope. Can't do that. If it's possible to declare any 2 properties with the same name inside the lexical scope of the class, the whole house of cards falls.

hax commented 5 years ago

@rdking Obviously you can't allow class X { let x; const x }, but I think class X { let x; get x() {} } may be possible, because you use x in class to access let x, and use this.x in class to call get x() {}. Though we'd better use linter to avoid that.

rdking commented 5 years ago

The problem with having a public and private member with the same name is that it completely imbalances any attempt at shorthand notation. Since there can be duplicate names, which one is going to be the one that gets accessed when you use the shorthand? This means you'd only be able to shorthand either the public or the private, but not both. But if you can't do both, it's not worth doing either.

hax commented 5 years ago

not sure, temporarily deleted.

rdking commented 5 years ago

Example of some pointless but perfectly valid code.

class X {
  let x = 1;
  let y = 2;
  constructor() {
    Object.defineProperty(this, "x", {
      configurable: true,
      get() { return x * 4; }
    }
    this.y = 7;
    y++;
  }
  print() {
    console.log(`(private) x = ${x}`);
    console.log(`(private) y = ${y}`);
    console.log(`(public) x = ${this.x}`);
    console.log(`(public) y = ${this.y}`);
  }
}

/* Prints
 * (private) x = 1
 * (private) y = 3
 * (public) x = 4
 * (public) y = 7
 */
(new X).print();
rdking commented 5 years ago

In general, any property declared within the class must have a unique name. All static members constitute a separate namespace from the non-static members. So it's possible to duplicate names that way. But within each of these namespaces, duplication is not allowed during class evaluation. Static members are properties of the constructor, so they are shorthand eligible only for static member functions.

Since all non-static members have unique names, they are shorthand eligible for non-static member functions and are added to the execution context of member functions as if the "with" keyword had been used. Properties added later (like in the constructor or outside the class) are not eligible for shorthand and require the full notation to be accessed.

mbrowne commented 5 years ago

@rdking You're making a big deal about the upcoming implementation change and saying it completely invalidates the closure concept, but I don't understand why. Aside from proxies forwarding to private members, it doesn't seem like much would change for developers in terms of the observable behavior of private members (you mentioned protected and friend earlier but I think that's a separate discussion). And since there will be an intermediate container object for private members, the notion of separate "private properties" doesn't precisely reflect what's going on under the hood anyhow.

Also, the use of shorthand syntax obviously makes it seems more like a closure, not less, so I'm not following your argument there. But I think the intuitiveness of let/const and shorthand syntax for access might be a good thing for this proposal anyway (although of course there's still the learning curve of understanding that these members are instance-level and not static).

hax commented 5 years ago

of course there's still the learning curve of understanding that these members are instance-level and not static

See https://github.com/hax/hax.github.com/issues/44 (note it use var in old classes 1.1, but we can agree let/const should have the similar result). So I don't think there's a high learning curve.

rdking commented 5 years ago

@mbrowne The original update did invalidate the closure concept. It just wouldn't work as long as it was possible to declare 2 different properties with the same name. However, as of the latest update, that's not a problem anymore. The mental model can go back to the closure concept. Like you said, the solution for the shorthand problem does indeed drive the model home.

rdking commented 5 years ago

The README for this proposal has been updated. As per @mbrowne's suggestion, information regarding the implementation details has been moved to "/docs/implementation-overview.md". There is a link to this at the bottom of the README.