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

Can we take a step back? #144

Closed rdking closed 5 years ago

rdking commented 6 years ago

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.

hax commented 6 years ago

@rbuckton

TypeScript currently does a Set instead of CreateDataPropertyOrThrow. While we might consider a flag for consistency with the proposed semantics down the line, we will always support doing a Set as we transpile down to ES3 which does not support Object.defineProperty (but also doesn't support accessors).

Actually, TS has a luck that type system could help! Compiler could (and should if eventually land such broken thing) warn users if they (accidently) shadow the getter/setter.

But still two bad luck:

  1. It can't save you inclass extends dynamicExpression
  2. Superclass could in dependecices which only have d.ts so you don't know it's an accessor!

Offtopic: Some pre-ES5 era browser support __defineGetter__/__defineSetter__ so in theory it still could be issue 😂

hax commented 6 years ago

@rdking

Since the common explanation I was given for class { x=1; } is that the assignment is moved to the constructor, I'm thinking that without considerably more explanation, most developers would think that the code I wrote was an ES6 version of a public field declaration in the current proposal. That, of course, leads to bad expectations.

Yes. This shadowing semantic is just missed because we never have such useless but dangerous thing in our history.

Most programmers will not get it in first place.

Only the guys who have very strong JS knowledge --- know every details about prototype, getter/setter and defineProperty semantic, and know this feature is called as "public field", and have a "correct" expectation there is a definition instead of assignment --- just like the guys who design this proposal, can get it in first place. 🙄

@bakkot If you want to land some "public field" anyway, you should land assignment semantic, and change the name to "class property initialization"....

mbrowne commented 6 years ago

@hax

Overriding the getter and setter would probably be OK

No it's not ok. Because you lose the functionality of the getter/setter. For example, base class may do the log.

Maybe I wasn’t clear. I was saying that this should probably be allowed:

class ExistingGoodClass {
  get x() {...}
  set x(v) {...}
}
class Subclass extends ExistingGoodClass {
  // override getter and setter, with the option of delegating to parent getter and setter
  get x() {...}
  set x(v) {...}
}

(And it’s a moot point anyway, since this is already allowed.)

But not this:

class Subclass extends ExistingGoodClass {
  x
}
rdking commented 6 years ago

@mbrowne We agree on that. What I've been giving was an explanation of why that's such a bad deal. @hax also seems to agree that this is not good behavior. Somehow, you two missed that you agreed with each other....

As for this issue, since ES class already has a means of overriding base accessor properties, having a new notation style that looks like and is taught as a simple property assignment but actually completely ignores prototypes during that assignment is a complete and total unexpected break from existing ES behavior. There is simply no way that such a departure will not end up as a "wtf!" for a large portion of JS developers.

mbrowne commented 6 years ago

What is this proposal’s rationale for having public fields use CreateDataPropertyOrThrow instead of Set? (I have been trying to follow this discussion from the beginning; did I miss something?) Is it just for consistency with how private fields work in this proposal?

At least on the surface, this ignoring of existing prototype properties seems like a very bad idea. While I am not against inheritance generally (in data models it’s often absolutely the right way to model things), it’s arguably the part of OOP that causes the most confusion to begin with and makes code far less readable (even the use of super can make code harder to understand). So it’s important to carefully constrain inheritance and we can see how it already causes confusion in languages that make heavy use of it. At the very least we should ensure that the JS inheritance model for properties doesn’t get any more complicated than it already is...

nicolo-ribaudo commented 6 years ago

Note that this proposal aligns with PHP, a language many JavaScript web developers are familiar with:

class Base {
  private $val = 1;
  public function __set($name, $value) {
    print "Setting $name=$value";
    $this->val = $value;
  }
  public function __get($name) {
    return $this->val;
  }
}

class Derived1 extends Base {
  public $x = 2;
}

class Derived2 extends Base {
  public function __construct() {
    $this->x = 3;
  }
}

new Derived1();
new Derived2();

That code only prints Setting x=3 (repl)

rdking commented 6 years ago

@nicolo-ribaudo That's all well and good for PHP. However, since it's inception, ES/JS has anything with this format keyword x=y be an assignment, not an accessor pair definition. Since the introduction of class, the get/set pair have been the way to override the functionality of an existing accessor property. class { x=1 } looks like the assignment of a data property, not an accessor property, so having class A { get x() {...} } class B extends A { x=1 } cause A.x to be completely ignored is counter-intuitive within the scope of this language.

bakkot commented 6 years ago

@mbrowne

What is this proposal’s rationale for having public fields use CreateDataPropertyOrThrow instead of Set?

see https://github.com/tc39/proposal-class-public-fields/issues/42. It's also discussed in the notes at great length on at least one occasion; I'm sorry don't have time to dig up the precise reference right now.

To me the most convincing arguments are

rdking commented 6 years ago

@bakkot

  • object literals already use define semantics

This one makes sense. It's a new object with no properties or prototype. Define is faster than Set because it does less. No issue here, but not at all justification for what the current proposal is trying to do.

  • class bodies already use [[DefineOwnProperty]] semantics, which is especially obvious for static class elements (e.g. class B { static set x(a){} } class D extends B { static x(){} } does not trigger the setter)

Again, this makes sense. A class body defines a prototype, which is just another new object. Also, it's no surprise that D.x doesn't trigger B.x. Static members aren't connected in that way. So this is also not a justification for the current proposal. In fact, it's an argument against it since the reason it makes sense to use [[DefineOwnProperty]] is because class is building a new prototype object. The public "fields" prescribed by the current proposal are not part of that object. Therefore, the justification to use [[DefineOwnProperty]] instead of [[Set]] doesn't hold.

  • any time this interaction would come up you're going to have to learn about it anyway

I don't get this argument. Where other than defining properties on a new object does this interaction come up?

  • [[DefineOwnProperty]] is the more performant and future-proof option. For example, [[Set]] semantics don't really make much sense in the context of decorators.

I beg to differ. Given that a decorator can do just about anything to a class definition, sometimes (reading and writing values) it makes perfect sense to use [[Set]] & [[Get]], while other times (adding new and removing existing properties) it makes more sense to use [[DefineOwnProperty]] & [[Delete]]. Trying to generalize that all onto [[DefineOwnProperty]] is what doesn't "really make much sense". So once again, not justification for doing something strange.

ljharb commented 6 years ago

Static members are connected in that way; in that example, a subclass that didn’t shadow the base class setter would trigger it when it was assigned to.

rdking commented 6 years ago

@ljharb Just tested that. Didn't expect that to work. Leaves me feeling a little weirded out, and at the same time disturbed that I cannot reach the shadowed member from the derived class constructor. Seems like a half-way solution. Oh well. Still doesn't change the premise of my argument that this isn't justification for what the current proposal prescribes for public fields.

hax commented 6 years ago

@mbrowne

Is it just for consistency with how private fields work in this proposal?

I think you catch it 😊

It's just another duality.

hax commented 6 years ago

@nicolo-ribaudo

Note that this proposal aligns with PHP

I always use PHP as a bad example....

But this time, it even better than this proposal, because:

They have KEYWORD which can tell programmers here is definition, which may override base class.

hax commented 6 years ago

@bakkot

To me the most convincing arguments are

  • class bodies already use [[DefineOwnProperty]] semantics, which is especially obvious for static class elements (e.g. class B { static set x(a){} } class D extends B { static x(){} } does not trigger the setter)

There is a keyword which tell programmers it's a definition. But class A extends B { x = 1 } has no keyword.

  • object literals already use define semantics

Subclass is not object literals. Irrelevant.

  • any time this interaction would come up you're going to have to learn about it anyway

???

When you comment other alternative proposal, you say blah blah blah, when I comment yours, you say:

YOU HAVE TO LEARN.

  • [[DefineOwnProperty]] is the more performant and future-proof option. For example, [[Set]] semantics don't really make much sense in the context of decorators.

This is valid. But it can't solve the trap.


You are just giving the reasons why it use definition semantic. Even they are all valid, it never solve the issue.

This is a false solution, because it's never a solution.

And please explain why you are so kind to the big, unsolvable traps your design introduce, but too critique to others?

mbrowne commented 6 years ago

What about simplicity? Despite the points @bakkot mentioned, I think it would by far be simplest and best if public field declarations were as close as possible to matching the current ES6 convention:

class Demo {
  constructor() {
    this.x = 1
  }
}

And this greatly simplifies things when considering inheritance, as others have pointed out above and which I'll expand on in my next comment...

mbrowne commented 6 years ago

Consider this quote from an article by James Coplien (OO expert since at least the 1980s) and Trygve Reenskaug (inventor of MVC and one of the earliest users of Smalltalk):

Adele Goldberg used to say, "In object-oriented programming, it always happens Somewhere Else." Part of this owes to the innate thesis of object orientation itself: that intelligence is collective rather than localized. This shows up in three ways: polymorphism, deep object hierarchies, and deep class hierarchies.

Most object-oriented thinkers will link Adele's quote to polymorphism, which is a kind of hyper-galactic shift in execution context that occurs with each method invocation. Polymorphism hampers our ability to undertand code statically: we can follow the sequencing of method invocations only at run time. It's perhaps no accident that there has been an increased focus on techniques like test-driven development with the advent of object-oriented programming: If you can't analyze, test.

Second, object hierarchies tend to be deep. More precisely, objects usually lack a hierarchical structure but possess more of the structure of the network paradigm of computation. To an architect who bases a system on units that interact via inter-process communication, object orientation has the feeling of message passing and of asynchrony. Objects in fact embraced the message metaphor explicitly; that it might infer asynchrony or parallelism is perhaps unfortunate. That detail notwithstanding, object orientation still has the feel of a pass-the-ball style of computation. This is a serious obstacle to program comprehension and intentionality because the program counter passes many abstraction layers on its way to accomplishing its goal.

Object orientation is designed so we are not supposed to know where the program counter will end up on a method call: object encapsulation and method selection insulate us from that coupling. We gain syntactic decoupling; we lose system-level comprehension. The supposed semantic decoupling of objects participating in a use case is largely an illusion, because in the end, each method executes in the business context both of the preceding and ensuing execution. It is difficult to reason soberly about a method in isolation, with respect to business goals.

(The article goes on to an even more important third point, but it's arguably off-topic.)

In 2015, as part of his work on DCI, Coplien created a research language (trygve) and said the following about his carefully constrained implementation of inheritance (source):

I’m being VERY careful with this. I thought long and hard before allowing inheritance at all. You can now use it, but it uses a more Smalltalk-style lookup than the horribly broken Java-style lookup with overloading across scopes… I borrowed many of the tyrannical scope locality conventions of C++ and strengthened them: there is no special relationship between base and derived classes with respect to access permission. I consider protected to be a violation of encapsulation of the base class and disallow it.

I am very firm about leaving the concept of super out of the language — I consider it to be broken. It’s fragile with respect to inserting new classes in the hierarchy (and other popular refactoring techniques). C++ does it right if you want to do class-oriented programming — which I do not.

This obviously reflects a pretty strict mindset and JS doesn't necessarily need to go to this extreme (and the ship has sailed on super), but the essential point is to help prevent the programmer from shooting themself in the foot wherever reasonably possible.

We should learn from Coplien's and Reenskaug's experience, wisdom, and knowledge of history. (And perhaps there would be some possibility for Coplien to share his perspective in a meeting with the committee if they were interested...I have no idea if this is feasible, just a thought that might be worth pursuing.)

Sidebar: DCI (invented by Reenskaug) is a really excellent paradigm that uses the best parts of OO, and also plays well with FP concepts like immutability if you choose to implement it that way.

doodadjs commented 6 years ago

"but the essential point is to help prevent the programmer from shooting themself in the foot wherever reasonably possible."

Should we stop making computers because they might be misused ?

mbrowne commented 6 years ago

@doodadjs

Should we stop making computers because they might be misused ?

This is a strawman argument. Note that I did not suggest that we eliminate classes nor inheritance.

rdking commented 6 years ago

@mbrowne Yes, it was a strawman, but the point he was trying to make is important. You can stab someone to death using a screwdriver. However, that's not the intended usage, and no one is bothering to protect anyone from that possibility. Sure, as a result, there are people who are actually killed with a screwdriver, but the utility of the tool would be severely reduced should a means to prevent stabbing someone be added to the design.

A programming language is just like that screwdriver. The purpose of the tool requires it to be able to allow you to create foot-guns if you so choose. There shouldn't be any foot-gun protection added to the tool since that will also reduce useful functionality. Sure, there will be people shooting themselves in the foot as a result. What did Thomas Edison supposedly say? "I didn’t fail 1,000 times. The light bulb was an invention with 1,000 steps." Making mistakes, even disastrous ones, is just part of the learning process. Reduce the potential to make mistakes and you reduce the ability to grow great developers.

mbrowne commented 6 years ago

@rdking I think it's more of a balancing act between power/flexibility and rules. For example, do you think the ability for JS programmers to accidentally create global variables just by omitting the var keyword was a good design choice? There are a lot of valid approaches to language design, and to your point there are examples like Python's complete lack of access modifiers (handling it all by convention). But too much freedom would be a source of unnecessary bugs, especially in the case of ambiguity like allowing the same property name to be redeclared differently in a subclass (and yes, I'm aware that some existing languages allow this but that doesn't mean it's a good design). There's enough to think about already just with inheriting the same definition from the parent class and the code in the constructors. Also I wasn't just talking about rules to constrain the programmer but keeping the design of the language itself simpler—that's why I agree that public field declarations should use Set instead of CreateDataPropertyOrThrow, which helps avoid unexpected behavior involving prototype vs. instance.

And I don't think anything I've suggested would make the language less powerful or flexible in any way other than preventing needless mistakes and confusion. As I said I'm in favor of wisely used inheritance, but consider the fact that the Go language doesn't have any inheritance at all and achieves the same goals via composition. I don't think this limits Go from expressing the same mental models as JS or any other language with inheritance; they're just programmed in a different way that ensures the execution flow is easy to follow.

hax commented 6 years ago

@rdking

Making mistakes, even disastrous ones, is just part of the learning process.

To some degree, I agree this.

But the problem is,

  1. The trap is very subtle, and even experienced programmers hard to discover it
  2. You can shoot yourself with no symptom (redefine a property from base class) and the bullet will kill you after many years (whenever the base class is refactoried to getter/setter). So before you eventually learned it, you may already shoot yourself hundred of times.
  3. Even you learned it, you can't assure everyone in the team learned it well and never forgot it. (Consider the syntax is too error-prone, you even can't assure yourself.) There is no possible linter can save you, TS type system can not fully help you, it's in practice impossible to check every field definition whether it shot or not. So you or someone in team just shoot, inevitably.

The only practical thing I can imagine, is teach everyone, a refactor from public field to getter/setter is a breaking change, but it shouldn't be. So the only reasonable solution is, discourage the usage of public field in the first place, always use old constructor way, or getter/setter wrap a private field though it's verbosely and ask for another new proposal for syntax sugar. I'll expect ESLint add such rule if this broken proposal eventually land. You are just adding a feature which community have to disabled soon.

Actually public field is mostly discouraged in Java/C# world, with some other reasons (relatively weaker than this), which may also apply to JavaScript.

rdking commented 6 years ago

@mbrowne Keyword-less variable creation wasn't just a bad idea, it was also a weird decision. I never understood what was intended with that choice. I'm pretty sure it probably had to do with some use case that involved being inside a function with the global object masked, but needing to put something on the global object. If that's not it, then I don't get it at all. The point is that allowing this was a decision that was made to enable a capability, not a choice to restrict some functionality. Even though it's a feature I avoid like the plague, I wouldn't dare have the unmitigated gaul required to deem it a "foot-gun" and remove it from the language.

You said "too much freedom", but how much is too much? What might be too much freedom for your needs may only be barely enough for mine. The backbone of both a free society and a programming language is the freedom to do what you want, leaving you to exercise an appropriate level of restraint. The only rule of law is the consensus that certain things are definitely bad and should be penalized. Notice that there isn't a single law in the land capable of performing preventative enforcement? Likewise, beyond the rules of syntax, there shouldn't be anything preventing you from coding a foot-gun and shooting yourself with it. Just expect to receive the pain of debugging when you do, and remember to write better tests next time!

Want to see a language with a simple design? Look as ARM assembly (choose your own chip). It doesn't get much simpler than that. Yet from that simplicity, a myriad of both good designs and foot-guns can be built equally. It's up to the developer to avoid the foot-guns, not the language.

I could go on for hours like this, but I'll boil it down this way. It is a language's responsibility to provide an ever increasing number of ways to simplify the ever increasing number of programming paradigms. It is a developer's responsibility to learn both how to maximize the efficiency of the code written in that language and how to avoid whatever pitfalls may be in the way. At no point is it ever the fault of the language if the developer shoots him/herself in the foot. That's why we write unit tests!

Any time a useful piece of functionality has to be blocked to prevent developers from making foot-guns, the language designer has done the developers a disservice by stealing away responsibility for creating good code.

rdking commented 6 years ago

@hax

The trap is very subtle, and even experienced programmers hard to discover it

Some 30+ years ago, we used to think of things like that as the dividing line between "experienced" and "expert". The goal was for every developer to reach "expert". Somehow, it feels like development has lost this goal. It's been replaced with trying to make a language so simple that anyone can program with only a minimum of training and come out looking like an "expert". I can't bring myself to see this as a good thing.

Also, I don't mind when obviously bad ideas are restricted by the language, as long as useful features aren't tossed out with it. That's one of the biggest issues I have with proposal-class-fields. The attempt to protect developers from potential foot-guns is outweighing the value of the features they're going to lose in the process, features that there's no good justification for blocking. They're only being sacrificed for the dubious ideal of preventing programmers from making mistakes.

I get that some mistakes are hard to find. I worked on such a problem at Wal-mart. They had the same bug for 15 years, passing it from one developer to the next, with no one being able to fix it before they handed it to me. I solved it in 2 months. I had to draw on the 20+ years of experience I had at that point in time to figure out what was going wrong between 5 different programs in 3 different languages acting on the same file. If I had spent those 20+ years not constantly shooting myself in the foot and learning from that, I probably would have been yet another developer to pass the bug on to the next person.

Summary. Foot-guns that can be removed surgically, without damaging other features are ok to remove. Foot-guns that cannot be removed without damaging other features need to be left alone, and developers should be expected to learn how to avoid the foot-gun.

rdking commented 6 years ago

@hax

Actually public field is mostly discouraged in Java/C# world, with some other reasons (relatively weaker than this), which may also apply to JavaScript.

Where public data fields are concerned, as long as they don't hold functions, I could care less about them. Where I'm concerned, any non-function exposed to the public in an encapsulating object should use accessors instead of a data property. The data itself should be tucked away in some private container. However, just because my experience has taught me that doesn't mean I have the right to prevent others from having the privilege of shooting themselves in the foot with the issues around that. After all, there are still a few use cases where such a public data property can be more useful than the alternative.

bdistin commented 6 years ago

I agree with the title of the issue, but I don't know where the actual argument being made is going?

Sure, a keyword would make it obvious and clear that something is being defined, rather than set. But then what would you suggest? I have seen var suggested somewhere in one of these threads, but that makes me think the variable will transcend the scope. let or public would be fine, but then you have inconsistency between method and property declaration, and you can't really change method declaration without breaking any existing class code. I would like to see constant properties with the const keyword, but that seems like something completely separate such as static const foo = 'bar' or private const meaningOfLife = 42... Can't really throw an error either, because as said above, it's not neccisarily unintended, if the author truely intends to overwrite the getter/setter pair.

So as much as I can see how the issue described is problematic, I can't really see a path forward that would make it clear you are defining something as opposed to setting something and not break existing js code. There has been a number of missteps that js has made in terms of syntax, and that has partially lead us to this very problem. The lack of planning, is directly responsible for this very issue, and this proposal's lack of planning is going to be directly responsible for a great deal more unaviodable problems (consistency, and clarity wise) in the future.

As I have read elsewhere, and I think it greatly applies in this situation: "Design is design and something you do once and have to live with." If tc39 keeps blinders on looking only at the proposals presented, and not at the big picture, we are going to keep having to live with these half baked inconsistent design choices.

When I think about the big picture of JS, I ask these questions:

  1. What is it that makes js great/my language of choice?
    • I personally believe dynamicism is JS's greatest strength.
  2. How does this proposal fit into the language, and does this effect 1?
    • The hash sigil forgets js's roots, and abandons dynamicism creating real consistency problems in the language
  3. Is this idea complete or the beginning of a new direction and how does this change where the language is going?
    • The idea of private is not complete. It's not enough to simply have hard private without a full suite of access modifiers. public, protected, private, const, friend class, final class, final method, and static varients. It's even more neccisary to share private state, than to just hide it in a single class, because that's the whole point of inheritence. Having a public api that is the public api you want to expose is great. But it's extrememly unexpressive to be forced to find a way to force all private state and behavior in a single class. As anyone who uses _convention currently knows derived classes and friends very much need to access private state and behavior to be concise and reduce code duplication. I am not saying you couldn't make a few single class utils that effectively use hard private state/behavior, but more often than not classes are meant to be extended and customized to the derived classes needs.
  4. If this is just the start of an idea, how do we ensure when the idea is fully realized, the syntax is consistent?
    • We should be planning for these other access modifiers, and not comming up with more half-baked inconsistent ideas when asked "well what about x". It's pretty sad we have nothing for public properties/methods, hash sigil for private properties/methods, and the prevailing idea is decorators for protected properties/methiods. 3 completely seperate syntaxes for the fairly standard 3 main access modifiers. Was not the lession learned that it's a really bad idea to make inconsistencies in the language, like no new when creating new Symbols?
    • Tbh, I have been a proponent of keywords, but to be honest if they were all decorators with an included standard library @​protected @​private @​final @​friend(OtherClass) ect that would be fine by me too. But then we are back to the idea that you must access them on this, which leads to problems this proposal tries to fix with the # sigil, which as @rdking correctly asserts breaks more basic and useful functionality in the attempt to "save" people from making mistakes. And imo, it breaks the single greatest thing that JS brings to the table, and the reason JS is my language of choice.

I get it, we are more or less stuck with no keyword for public properties and methods because that design has already been designed, it's done, and we have to live with that. But where this proposal takes private and future modifiers, we can do something about that now, and we should be looking at that now. Not later when this proposal has made it impossible to be consistent with, and the proposal after that making later proposals likewise inconsistent and even more unclear.

bakkot commented 6 years ago

@bdistin,

The hash sigil forgets js's roots, and abandons dynamicism creating real consistency problems in the language

This proposal provides almost exactly the same sort of privacy currently offered by closed-over variables, which has been used in an extremely common pattern for private instance state for over a decade.

I do not understand why this seems so different.

(Forgive me if I've had this conversation with you before; I can't keep track anymore.)

hax commented 6 years ago

@bakkot

This proposal provides almost exactly the same sort of privacy currently offered by closed-over variables, which has been used in an extremely common pattern for private instance state for over a decade.

I think it's obviously such pattern is more close to classes 1.1 design.

I do not understand why this seems so different.

You always underrated the destructive power of # to people. Yeah, that's the root cause of all.

See my another try for summary: https://github.com/tc39/proposal-class-fields/issues/142#issuecomment-430494935

mbrowne commented 6 years ago

@rdking I don't think we're going to see eye to eye on this, but you seem to think I'm suggesting tyrannical constraints, so let me clarify...

If we look at software trends over the past few decades, it's clear that programmers no longer understand how their programs work at run-time as well as they once did. An explosion of essential complexity is probably the main factor, but the rise of OO is another: there are often many potential receivers of a given method call or property access, so reasoning about the code became exponentially more complex. But we don't need to throw the baby out with the bathwater: OO is still one of the best tools we have for modeling the real world in code.

And we shouldn't just be concerned with OO, but also procedural, FP, etc. One of the key strengths of JS is that it's a multi-paradigm language, and I very much agree that that freedom is vitally important. People invent all kinds of creative and brilliant ways of using it. A good case in point is actually DCI, which IMO is superior to any other paradigm for certain use cases. Even though the language wasn't designed for it at all, JS is one of the languages where it's easiest to implement because it's so flexible and dynamic. (There are still some shortcomings of the implementations that are possible in JS, but I don't expect that the features for a perfect implementation would be added unless there were community demand for it.)

At no point is it ever the fault of the language if the developer shoots him/herself in the foot. That's why we write unit tests!

@hax showed how one of the foot-guns we've been discussing could cause problems years down the line, and it's naiive to believe that unit tests will always save you (or integration tests...). If you really want the freedom to make lots of mistakes you can program in C. By enforcing a few reasonable limits, JS can easily support multiple paradigms and a great degree of freedom without introducing unnecessary sources of subtle bugs. And actually you already have full freedom anyway via Object.defineProperty and soon, decorators.

Another way of thinking about this is essential vs. accidental complexity. We can give full power to program solutions to essentially complex problems with a myriad of paradigms without introducing the accidental complexity of misunderstandings or poor design that doesn't work in any paradigm.

mbrowne commented 6 years ago

The lack of planning, is directly responsible for this very issue, and this proposal's lack of planning is going to be directly responsible for a great deal more unavoidable problems (consistency, and clarity wise) in the future.

+10

I think this comment from @bakkot demonstrates this lack of planning:

I am not strongly opposed to private #x. But we have discussed it several times and always come down against it. And I do think it would be a shame to end up in a place where people learning JavaScript three decades hence see a private keyword there which is totally unmotivated by anything which actually ended up in the language.

I actually agree with @bakkot's reasoning if we could be reasonably confident that we won't need native support for other access modifiers. But we can't be confident of that at all: it seems like no one has thought this all the way through and really designed the language in a holistic way. I totally understand and agree that we need to develop the language incrementally and leave room for future competing proposals, but we definitely need more of a vision than, "decorators will fill in the gaps and if anything more is needed, we'll figure it out later." This is even more important considering that public methods already don't have a keyword. We can't have two defaults: public is the default access level for JS, so it's the other access modifiers that should be preceded by keywords...or else not have any of them preceded by keywords. If the main argument against private #x is that we don't know what we'll need in the future, then it's contradictory: it's assuming that we will NEVER need keywords for other access levels, as if we know the future absolutely.

mbrowne commented 6 years ago

@bakkot you said the following in your comment in the other thread:

There's also been discussion of allowing private fields to be declared outside classes, in which case two classes in the same scope could share visibility of such a field.

Can you point me to that discussion? Perhaps between that and decorators, we in fact would not need modifier keywords in the future (my advocation of private #x hinges on that).

mbrowne commented 6 years ago

After all, there are still a few use cases where such a public data property can be more useful than the alternative.

I agree with @rdking's statement. Public properties are not necessarily bad, and getters and setters everywhere are a hallmark of lack of encapsulation, which is possible even when all data properties are private. Also, JS wouldn't suffer from the problems with public properties to the extent that Java and C# do because in JS you can always refactor later to use a getter/setter without affecting the consuming code. BTW this is a major reason why the foot-gun @hax mentioned should be prevented.

bdistin commented 6 years ago

@bakkot

It's not about being semantically close to scoped variables. My single biggest complaint is about parity with public/this comment + the next 4 comments below it details out most of what I am talking about. Computed property access is almost, if not as old as the language itself. This proposal abandons that and more, for this sole feature, to "save" people from making a mistake.

bdistin commented 6 years ago

@mbrowne

I actually agree with @bakkot's reasoning if we could be reasonably confident that we won't need native support for other access modifiers. But we can't be confident of that at all...

I can make numerous arguments for other modifiers. Take final for instance... Do you know hard private is pretty much useless as a security feature without final?

Some api wrapper lib:

class Client {
  #token

  // must be some public way for users of the library to provide their api token
  async login(token) {
    // we need the token for further REST api requests
    this.#token = token;
    // login to the api (make a websocket connection)
  }
}

some evil extension of the library:

class EvilClient extends Client {
  async login(token) {
    // await posting the token to Pastebin
    return super.login(token);
  }
}

It doesn't matter that you are protecting token from further access if you can't declare the login method as final async login(token) since that token can always be intercepted by an intermediary library extension.

mbrowne commented 6 years ago

@bdstin Did I fairly represent your feedback in this quote in #150?

will irreversibly reduce the design space for important future proposals such as final keyword and access modifier keywords

It occurred to me after posting that that I don't understand why this proposal would prevent the future implementation of final, and maybe that's not what you meant.

hax commented 6 years ago

@bdistin

Do you know hard private is pretty much useless as a security feature without final?

Actually, there is Proxy which basically can intercept everything...

bdistin commented 6 years ago

Moreso consistency with future access modifiers, final is a bit separate, even though it dictates extensibility.

Actually, if I am being quite honest, it is so separate, I wouldn't be opposed to final being a decorator so that it could be consistently applied to both whole classes and individual methods.

protected for vertical shared encapsulation and friend classes for horizontal shared encapsulation are the bare minimum additional features I see as the natural future of js after private. (along with some form of final to protect methods and classes from being extended)

If I am not mistaken you once even suggested a softPrivate access modifier, and elsewhere I had seen someone suggest a hidden access modifier. Although I am personally not sure how much more value those two offer beyond the standard public/protected/private model, design space for them could be important in the future. But even so, in one comment I suggest that all could be done in the future with keywords: comment

bdistin commented 6 years ago

@hax

unfortunately, that's true. I don't have a good idea on how to prevent that without adding more syntax... Perhaps that could be the meaning of the hidden keyword or another word like it.

@final
hidden async login(token) { }

makes the login method hidden from inspection via proxies. I could see value in an access modifier that can offer protection from proxies.

edit: On second thought, that idea more decorating the method, rather than needing to separate it into a different "hidden" namespace...

bakkot commented 6 years ago

@mbrowne:

I actually agree with @bakkot's reasoning if we could be reasonably confident that we won't need native support for other access modifiers.

OK, then allow me make that claim. I am reasonably confident that we won't need native support for other access modifiers. If we never touch classes again after this proposal and decorators, I will not think of them as incomplete.

The core semantics in this proposal stands on its own, and decorators give you huge flexibility to write your own accessibility modifiers between public and private. I do not think we should sacrifice the design of this proposal just to leave space for a feature which we do not necessarily want and, more importantly, should not design in advance of shipping other features which allow people to feel out that design space and its limitations in userland.

it seems like no one has thought this all the way through and really designed the language in a holistic way.

Here, you are asking that we design the space for all future modifiers now. Elsewhere, @hax is asking that we drop public fields entirely from the current proposal, and maybe we can figure it out later if we want. These demands are pretty much precisely opposite.

I think this proposal strikes a good balance between adding enough that we don't unduly spec ourselves into a corner and not adding so much that we get it wrong.

mbrowne commented 6 years ago

@bdistin thanks for the clarification. I removed the part about final from #150, since it's kind of separate.

rdking commented 6 years ago

@mbrowne

I don't think we're going to see eye to eye on this, but you seem to think I'm suggesting tyrannical constraints, so let me clarify...

Maybe we won't, but that's what makes an argument fun. When it comes to language development, I think there are simple rules that must be followed, which the current proposal is breaking.

  1. Identify the issue or desired feature.
    • ✓ We all want some simple syntax-supported means of putting data on an object that cannot be accessed except via functions defined on that object at the time of definition.
  2. Identify any hinderances and considerations due to the existing nature of the language.
    • ✗ Has this even been done?
  3. Identify the must have requirements of the desired feature.
    • ✗ Has this been done? No such list has been produced.
  4. Identify the additional "wants" or "good to have"s of the desired feature.
    • — Some progress has definitely been made here.
  5. Identify any conflicts between the existing language and the requirements + wants.
    • — Some progress has been made here.
  6. Resolve conflicts favoring existing language features except where absolutely necessary.
    • ✗ Big fail here due to foot-gun issue elimination being favored over existing features, and again for lack of consideration for Proxy while private fields are supposed to be undetectable.
  7. Create syntax that is consistent with the existing flow of the language.
    • ✗ Big fail here due to the use of a non-keyword for declaration, and again for a non-operator, non-identifier, non-literal character in an access operation.

This is essentially what I think of the current proposal. It's not even half-baked yet, but somehow, it's in stage 3?

bakkot commented 6 years ago

@mbrowne:

Can you point me to that discussion?

I can't dig it up anymore, I'm afraid. I (and other people, other times it's come up) am imagining something like

private #x; // this declarative form can be used in any statement list, and is visible only to the block scope. it does not permit an initializer.

let obj = { #x: 0 }; // object literals can have `#x` as a property name

obj.#x = 0; // as long as `#x` is in scope, you can read and write to it on objects which have the field

otherObj.#x; // as in the current proposal, attempting to read or write a field on an object which lacks it will throw

let f = o => o.#x;
console.log(f(obj)); // prints 0 - as in the current proposal, all code referring with visibility of a private declaration refers to the _same_ name

class A { #x = 0; } // classes can install a private name visible in the outer scope onto their instances

{ #private #x; } // as with any declaration, you can shadow outer declarations in nested scope
class B { private #x = 1; } // as a convenience, classes can also declare private names in their own scope

#y; // as in the current proposal, referring to a name which is not in scope is a syntax error

Alternatives include letting class A { #x = 0; } be a declaration by default and instead having a keyword in classes (like use #x = 0) to indicate that they're intending to reuse an outer declaration rather than redeclaring and shadowing it.

Pretty much like private symbols, without the problems of actually reifying a private symbol and with the current proposal's access semantics (throwing instead of going up prototype chains, etc).

mbrowne commented 6 years ago

Here, you are asking that we design the space for all future modifiers now. Elsewhere, @hax is asking that we drop public fields entirely from the current proposal, and maybe we can figure it out later if we want. These demands are pretty much precisely opposite.

I don't think our demands are as opposite as they apparently seem. @hax argues that we don't need public property declarations, but I think he would probably agree that if they are on the table, we should think through how they would work even if they wouldn't be added until a follow-on proposal. And I'm not saying that the committee should design everything up front. Say there are 3 different viable paths forward that we can envision and we're not even 100% sure that any of them will happen—that's fine. But we should at least envision them and make sure that the current design doesn't unnecessarily paint us into a corner. As @bdistin pointed out, couldn't a little more planning in past decisions have helped avoid some current design limitations in the language—ironically including the fact that the committee believes # is the best option despite wishing a more intuitive syntax were possible?

I am reasonably confident that we won't need native support for other access modifiers.

decorators give you huge flexibility to write your own accessibility modifiers between public and private

Decorators are great, but how can we be at all sure that they will be sufficiently ergonomic and performant for access control?

I think the idea you described above to declare private state outside of classes is a much more convincing argument. And plus it uses private #x instead of just #x so now I'm unclear: what is your opinion on this idea, and do you see the shorthand #x = ... still being used to declare private fields within classes in combination with this idea?

rdking commented 6 years ago

@bdistin The problem with your approach is that the login method must be called. If it's an implementation detail, then something else must call it with the credentials. At some point, something public will need to handle the credentials, and that public thing can be overridden. The idea behind "final" is that this thing is non-configurable and non-writable. That way even a derived class cannot override it.

rdking commented 6 years ago

@mbrowne @bakkot That looks like one of the discussions from about 2 years ago, the "private-names" proposal...

bdistin commented 6 years ago

Yeah, the hidden access modifier idea was brainfart. You would need something like final for protection against inheritance, and something else (maybe a decorator) designed to protect against proxy interceptions.

bakkot commented 6 years ago

@mbrowne:

I don't think our demands are as opposite as they apparently seem.

I really think they are. Moving to the 1.1 proposal means forgetting about the work we've done for public fields. If you start from the point of view that any proposal should explicitly leave room for public fields, the classes 1.1 proposal makes much less sense.

Decorators are great, but how can we be at all sure that they will be sufficiently ergonomic and performant for access control?

"sufficiently" depends on your use case. But like I say, I don't think any other access control modifiers are likely to prove necessary; if you want something more, and are willing to pay the cost decorators incur, you are welcome to, but I don't think the language needs to build in more support than that.

And plus it uses private #x instead of just #x so now I'm unclear: what is your opinion on this idea,

I am neutral on it.

do you see the shorthand #x = ... still being used to declare private fields within classes

Either approach I outlined seems viable to me, though I know there's disagreement on this point.

hax commented 6 years ago

@bakkot

The core semantics in this proposal stands on its own, and decorators give you huge flexibility to write your own accessibility modifiers between public and private.

Use the same rationale, we only need privacy solution, and decorators can help you to create a getter/setter. So why we have to land a public field which have unresolvable footguns?

bakkot commented 6 years ago

@hax I do not think that classes without public fields would stand on their own.

hax commented 6 years ago

@bakkot

Here, you are asking that we design the space for all future modifiers now. Elsewhere, @hax is asking that we drop public fields entirely from the current proposal, and maybe we can figure it out later if we want. These demands are pretty much precisely opposite.

NO. My point and his point are never opposite.

If you want to land only essential feature, then you should just land privacy solution, leave all other things to user land. If you want to land full features for user convenience, then you should design it all.

What I'm really asking is, DO NOT USE DOUBLE STANDARDS. This proposal is too fuzzy because it use too many double standards.

hax commented 6 years ago

@bakkot

@hax I do not think that classes without public fields would stand on their own.

You are just repeating your opinion I already know. Thank you.