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

Turning "hard private" into "soft private" #189

Closed jwalton closed 5 years ago

jwalton commented 5 years ago

In which we provide a fairly trivial way to bypass "hard private" protections on third party libraries, in practice.

Is this really "hard private"?

It seems like a lot of decisions in this proposal concerning private properties have been made specifically to support "hard private" properties. There was a long discussion here about whether this was a good idea or not, which didn't really seem to come to a consensus. TC39 is never the less proceeding with "hard private", and I see a decisions being justified using the argument "the alternative wouldn't really be hard private." This is stange to me, because the current proposal isn't hard private (at least in practice), and I suspect creating hard private properties is impossible in JavaScript.

What is "hard-private" anyways?

To quote @ljharb:

If it's accessible, it's public. "soft-private" is no different than underscores, because both are a convention (albeit, the former would be a convention baked into the language).

If a user can do it, they will, and if you break them, you broke them, even if they did something unwise.

(We can have a whole argument here between the "programming by contract" people, and the "language purist" people, about whether that second statement is correct or not - in fact we did in https://github.com/tc39/proposal-private-fields/issues/33, so let's not have it again. 😉)

Obviously, nothing is really hard-private. I mean, the contents of that private variable are somewhere in memory, and I'm sure I can find some interesting clever way to get at them. I can write my own javascript engine, or write a node.js native module that peeks at memory, or I can use some esoteric attack like rowhammer to exfiltrate that data. What we really mean by "hard private" is that, in a "normal" javascript environment (not a debugger, and maybe in a browser), you can't get access to the contents of a private member without doing something extraordinarily.

The motivation for hard private seems mainly (?) to be for library authors; so let's say if a user can access private members in your library "easily", it's not hard private.

So, how "hard private" is this proposal?

PoC||GTFO

Bob writes an npm module:

export default class MyClass {
    #secret = "You can't see me!";
}

Alice is using Bob's npm module, and realizes she wants access to that #secret variable. I mean, it's named with a hashtag, and hashtags are for sharing, right? (And, let's be honest, this is a pretty boring NPM module. What else is Alice going to do with it?)

const inst = new MyClass();
console.log(inst.#secret);  // No worky worky.  Sad tombone.

In order to get around this, Alice adds the following to her .babelrc.js:

module.exports = {

  ...

  overrides: [{
    test: ["./node_modules/bobs-package"],
    plugins: ["private-class-fields-to-public"]
  }],
}

babel-plugin-private-class-fields-to-public is a Babel plugin which transforms Bob's package to look like this:

export default class MyClass {
    #secret = "You can't see me!";
    get _secret() { return this.#secret; }
    set _secret(secret) { this.#secret = secret; }
}

Now Alice can write:

const inst = new MyClass();
console.log(inst._secret);

Mischief managed!

Wait wait. Isn't this cheating? Babel isn't a "normal javascript environment!"

Between @babel/core and babel-core, babel was downloaded around 11 million times in the past week. Not too many javascript programs these days don't go through babel. You could even run babel in a browser, and transpile third party modules on the fly. babel-plugin-private-class-fields-to-public plugin was written in typescript, which for a while was a big babel competitor, but even that plugin was compiled with babel. Seems like a normal javascript environment to me.

Even if you don't buy that argument, look at this from a practical standpoint:

In Python, private members are about as soft-private as you can get. Private members are members that start with an _. To get around this, you need to type the name of the variable, and maybe feel bad for a few moments.

Java, on the other hand, is still soft-private by the "if it's accessible" definition above, but you have to do some work to access a private member. You have to go look up the refletion package and figure out how it works again (since the last time you used it was probably when you last had to get around a pesky visability problem). Then, when it doesn't work, you need to go to stack overflow to find out you forgot to call setAccessible().

With this proposal, as it stands, accessing private members in JavaScript falls somewhere between these two; you need to install a babel plugin, but after that it's basically "type _ and feel slightly guilty". Let's put it somewhere between Python and Java; maybe not as "hard private" as all that.

ljharb commented 5 years ago

Hard private is not “defeated” if you edit the source - either manually or programmatically. The concern is about at runtime - if the code shipped to the engine has encapsulation, then it will be reserved (just like with variables inside a closure).

jwalton commented 5 years ago

Really? I admit that installing a babel plugin might be, as you put it "unwise", but if a user can do it, they will, and here they certainly can. At the end of the day, JavaScript is an interpreted language; if you want to share a module with me, you need to share the source, and I can infer the existence of private variables by reading that source. So can my program. I can even write a loader that reads in your module and does this at execution time.

(Anyways, hopefully I at least got a grin out of you with my hashtag comment. ;)

ljharb commented 5 years ago

If they’ve done that, they’re not actually using the feature. Actually using the feature - shipping the code using private fields that are not intentionally exposed - results in “hard private”, which is what matters.

jwalton commented 5 years ago

Isn't this is like saying "If you type otherObject._something in python, you're not using the features in PEP 8 - actually using the feature, and not typing the _, results in hard private"?

I mean, the "feature" here is a feature for library developers to prevent library users from accessing private members in their library. It's not really a feature for library consumers at all. If there's a simple way for library consumers to get around that protection, I'd argue that it's not "hard private".

ljharb commented 5 years ago

A source rewriter isn’t a simple way, and most importantly, it doesn’t work at runtime.

jwalton commented 5 years ago

People will have the source rewriter installed if they even want to use a module that uses private variables, at least for the next several months.

And why do you think it doesn't work at runtime? Babel is just a javascript program; it's as simple as replacing your import with:

    const moduleSource = await fetch('https://domain.com/bobsPackage.js');
    const transformed = babel.transform(source, {
        plugins: [privateToPublic],
    });
    const bobsPackage = eval(transformed);
    console.log(bobsPackage._secret);

It even works in a browser.

robpalme commented 5 years ago

@jwalton Thanks for the entertaining insight. It's good to highlight this.

My view is that it all comes down to boundaries. If you don't define boundaries, then you can make arguments that any encapsulation is not truly "hard" (not a true Scotsman) unless it's on an air-gapped machine that can only be communicated with via unequally-laden carrier pigeons.

The boundary of ECMAScript is the source text ingestion. So it's fair to say that details are not abstracted away from anyone that can tamper with code prior to ingestion. Therefore any judgement of whether something is truly encapsulated by ECMAScript should be assessed post-ingestion.

A secondary and lesser point: Real-life use of fields will tend to go through minifiers that will mangle private names in ways that (underscore-prefixed) properties cannot safely be mangled. This will act as a deterrent to casual/widespread use of the babel-plugin-private-class-fields-to-public approach because mangled names are harder to deduce and can change frequently.

A final and least important point (because it all just flows back to top-level philosophy): as an electrical OEM who manfactures computer PSUs locked by uncommon star-shaped screws and covered with "danger of death" stickers, changing the internals of the PSU is a reasonable thing to do. If a user gets inside and has a bad experience because they didn't understand the updated internals, we all know who is at fault. If that same PSU just has the stickers but no physical lock, it would be more of a grey area and some (in places with consumer regulations) would argue it's an accident waiting to happen. The Babel plugin is an invasive power-tool akin to a star-shaped screwdriver IMO.

jwalton commented 5 years ago

@robpalme Thanks for the insightful comments. These are all fair points.

And, one deterrent to the use of babel-plugin-private-class-fields-to-public that you missed, at least in the short term, is that most libraries published to npm have already been through babel once, and don't have their original es6 source published; if your private fields have already been turned into a WeakMap, then there's no way for my little babel plugin to find them.

But, in reference to your last point; I would point out that in, say, Java, once you break out the reflection API, you're pretty clearly tearing through "danger of death" stickers. You have very definitely violated the contract put in place by the author of the module you're using, and you know that if bad things happen later on, it's totally your fault. So I would call Java a "hard private" language, if this is where we're drawing the line. (Is Python like one of those cheap "Warranty Void If Removed" stickers that's already started to peel up at the edges when you take the product out of the box?)

But we software types (as a generality, and like all good generalities it's not always true) tend toward the tinkerer type - the sort of people who think up a pretty funny joke about hashtags and private members, and then spend an hour learning how to write a babel plugin just so they can make that joke. Folks like us are going to break out the power tools and star shaped screw drivers from time to time.

Ultimately, I guess, the point of this issue is, if we're going to make design decisions and say "we will make this extra complicated in service of the very important goal of hard privacy," but then we don't actually get the benefits hard privacy, was that complexity worth while? The answer to that question - where and what tradeoffs are "worth it" - is going to be different for everyone, of course, and maybe it is worth it. But hopefully I've given people something to think about over the holidays; once this makes it to stage 4, it's forever.

littledan commented 5 years ago

I'd say that running a Babel transform like this is a way of forking your dependency, which is a reliable way to get at private state. The key is, when forking a dependency, you are taking on the burden of maintaining that fork. By contrast, many large JS projects have had to roll back changes when there were too many compatibility complaints from people accessing "internal" APIs. I'd say this is a qualitative difference.

jwalton commented 5 years ago

@littledan I understand the point you're making, but I think the line you're drawing here is a little murky, and is only going to get less clear as time goes on. Babel added the overrides keyword to their config specifically to make it easy to transpile dependencies.

In our product, we're already transpiling the very popular debug library because they now publish code with es6 features to npm. Our toolchain for an older part of our product has an ancient i18n string extractor, written by a former co-op, which is totally unmaintained and breaks on the const keyword, so we transpile debug all the way down to ES5, so it doesn't crash our i18n preprocessor. We don't even rely on debug directly - our dependencies rely on debug, but that's enough to get the code into our bundle. (Wow, I really need to fix that. That's something I should work on while I'm at my in-laws for the holidays.)

But my point is, there are a lot of advantages to transpiling dependencies, and it's only going to get more popular to do so as time goes on.

And again, to look at this from a purely practical standpoint; if there's a library foo, and foo's users constantly violate it's privacy restrictions, that's a pretty good indicator that the author of foo is perhaps being a bit too restrictive in what they expose. If, in order to be useful, users of foo need to violate privacy restrictions, then they will figure out a way to do so. Life finds a way. If people are violating the privacy restrictions en-masse, and the package maintainer makes a change but it breaks the Internet, Wreck-it-Ralph style, then on that day when we are all huddled in our cold, dark homes, unable to control our IoT thermostats and lights, when Alexa no longer fills our orders for milk and we are unable to feed ourselves, will it really matter if we can say "This is our own fault, because we didn't have hard private variables in JavaScript, and someone posted a popular stack overflow answer that told people to use foo._widget!" Or will we somehow be saved if we can say "We gave the world beautiful perfect hard privacy. It's totally not our fault that someone posted a popular stack overflow answer that told people to install a babel plugin and /then/ to use foo._widget"?

superamadeus commented 5 years ago

Unintentionally depending on a library's "soft-private" api is easier than you'd think. For example:

My software depends on library@1.0.0.

// node_modules/library@1.0.0

class Library {
  /* ... */

  getBooks() {
    return [ /* ... */ ]
  }
}

// index.js

class MyLibrary extends Library {
  getBooksByAuthor(author) {
    return this._getBooksWithFilter(b => b.author === author);
  }

  _getBooksWithFilter(filterFn) {
    return this.getBooks().filter(filterFn);
  }
}

Ooh, Library@1.1.0 was released! I'll upgrade because features I want have been added in a non-breaking way (according to semantic versioning).

// node_modules/library@1.1.0

class BookFilter {
  applyFilter(books) {
    return books.filter(/* ... */);
  }
}

class Library {
  /* ... */

  getBooks() {
    return [ /* ... */ ]
  }

  _getBooksWithFilter(filter) {
    return filter.applyFilter(this.getBooks());
  }
}

Suddenly my software is not working as expected. How is this possible? It was a minor version change! This library is unpleasant!

zenparsing commented 5 years ago

@superamadeus This is a good use case for symbols.

superamadeus commented 5 years ago

@zenparsing Alternatively, private fields.

Edit:

// node_modules/library@1.1.0

class BookFilter {
  applyFilter(books) {
    return books.filter(/* ... */);
  }
}

class Library {
  /* ... */

  getBooks() {
    return [ /* ... */ ]
  }

  #getBooksWithFilter = (filter) => {
    return filter.applyFilter(this.getBooks());
  }
}
ljharb commented 5 years ago

Note that the semver spec is a bit ambiguous about whether that change can be semver-minor - are symbols and underscores part of the documented API or not? If by “documented” you mean “what i can see in the repl”, yes - if you mean “what’s written in prose in another place” then probably not. I interpret it in the former way, to protect my users from any unintentional breakage; some authors are less cautious. Private fields make this unambiguous.

zenparsing commented 5 years ago

In my opinion symbols are a better fit for the library use case.

Why?

superamadeus commented 5 years ago

@zenparsing

I appreciate your opinion and see where you're coming from. That said, I think both options (symbols vs. private fields) are viable and would require forethought about the tradeoffs. But neither solution will work for 100% of the use cases.

In my opinion symbols can get verbose:

// private field
class Service {
    #myMethod1 = () => {};
    #myMethod2 = () => {};
    #myMethod3 = () => {};
    #myMethod4 = () => {};
    #myMethod5 = () => {};
    #myMethod6 = () => {};
    #myMethod7 = () => {};
    #myMethod8 = () => {};
    #myMethod9 = () => {};
}

// symbols

const myMethod1 = Symbol();
const myMethod2 = Symbol();
const myMethod3 = Symbol();
const myMethod4 = Symbol();
const myMethod5 = Symbol();
const myMethod6 = Symbol();
const myMethod7 = Symbol();
const myMethod8 = Symbol();
const myMethod9 = Symbol();

class Service {
    [myMethod1] = () => {};
    [myMethod2] = () => {};
    [myMethod3] = () => {};
    [myMethod4] = () => {};
    [myMethod5] = () => {};
    [myMethod6] = () => {};
    [myMethod7] = () => {};
    [myMethod8] = () => {};
    [myMethod9] = () => {};
}

For the record, I'd rather use private methods (#myMethod() {}).

jwalton commented 5 years ago

@superamadeus This is a really helpful example. For this particular example, you could make getBooksWithFilter(self, filter) a top-level function and just not export it. But, I can certainly think of cases where I have a function that I want to be a bound member function, like say a React event handler where I want a per-instance bound function so I can take advantage of PureComponent. And there you are quite likely to have name collisions like this, because people tend to name their functions onSubmit or other such boring things. :)

jwalton commented 5 years ago

(And, of course, data instead of functions. Don't want to be stashing member data in global variables.)

zenparsing commented 5 years ago

@superamadeus That's why we ought to have sugar over the symbol-internal-to-module pattern.

If we had sugar over that pattern, and we assume that that pattern works for the library use case, then what use cases are left for hard private?

jwalton commented 5 years ago

This made me think about this problem in a very different way. It sounds like what we really want here isn't "private" variables, but "class scoped per-instance variables". But we already have keywords for declaring block-scoped variables in JS; const and let:

class MyLibrary extends Library {
  // Declare a per-instance variable that's only accessible within these {}s.
  let getBooksWithFilter = (filterFn) => {
    return this.getBooks().filter(filterFn);
  };

  getBooksByAuthor(author) {
    // Note that `this._getBooksWithFilter` is undefined here.
    return getBooksWithFilter(b => b.author === author);
  }

}

That would make this kind of a unique-to-javascript concept, but the # private thing is also kind of a unique-to-javascript concept. And it de-weirds things considerably - there's no this.#foo !== this['#foo'], for example. The one downside this would have over the # version is that you can't easily access these variables from other instances of the same class.

superamadeus commented 5 years ago

@jwalton I don’t really see what this gains. The syntax with the # sigil is jarring at first because it’s new syntax but it’s very consistent if you don’t try to compare it to typical property access.

In the example you’ve shown, it is hard to tell at call site if you’re using a private member vs a global variable. I personally like knowing when I’m making a private member access.

Edit: but yeah, I think the private field mechanism is very similar to “class scoped per-instance variables”. I don’t see how what you’ve described makes any gains on the current proposed syntax.

hax commented 5 years ago

The one downside this would have over the # version is that you can't easily access these variables from other instances of the same class.

@jwalton This can be solved by introducing a new operator like other::x, or just use non-sugared form other[symbol] if it's symbol-based.

superamadeus commented 5 years ago

@zenparsing @hax It was my (assumed) understanding that the committee had considered a symbol-based approach, and declined it. I can't find any documentation on this though. Anyone have any info on this?

That said, I still fail to see what you gain from the symbol based approach that you don't get in the current private field spec.

I'd love to see some examples of cases where the symbol approach provides benefits over the current private field spec, with respect to hard-private fields. I'm having trouble getting there on my own.

jwalton commented 5 years ago

@superamadeus There's one thing that's always bugged me about the current private property proposal:

class Library {
  #books = ['Snow Crash'];
  listBooks() {
    return this.#books.slice();
  }
}

const library = new Library();

library.addBook = function() {
  this.#books.push('Mistborn'); // Nope!
}

I'm told this doesn't work. Even if you define new functions on Library's prototype, they can't access the private members of Library. That's really odd. I can't think of another language that's like that; private means not only private to the instance, but private to scope as well. The let syntax is maybe functionally nearly identical, but I think semantically it expresses the intent much better.

It's very natural to try to compare # to natural property access, because it looks like a property access, when really it isn't.

ljharb commented 5 years ago

@zenparsing your conclusion rests on soft private actually being sufficient or good for libraries by default - my experience tells me that it is not, even if maintainers initially believe that it is.

zenparsing commented 5 years ago

@superamadeus

I'd love to see some examples of cases where the symbol approach provides benefits over the current private field spec, with respect to hard-private fields. I'm having trouble getting there on my own.

@jwalton points out one benefit: with symbols you don't need to cram everything that references the property into the class body. You can put the functions where they make sense.

Another advantage: with symbols you can trivially wrap an object with a Proxy and it works. With private fields, you have to create a complex membrane around it in order to avoid TypeErrors.

Another one: with symbols you can easily create a generic deep clone by using Reflect methods (like Refect.ownKeys). With private fields, you have to embed cloning logic directly in the class body, for each class.

Or imagine trying to write your own "object inspector" in normal JS. With private fields you can't get to the data values to display them.

zenparsing commented 5 years ago

@ljharb Can you elaborate on why symbols aren't good enough for libraries?

ljharb commented 5 years ago

@zenparsing any time something is accessible externally, it increases the api surface. Symbols are still accessible, and if it’s possible to depend on them, people will (and do).

I have dealt with tons of issues on many libraries i maintain due to people depending on things that weren’t intended to be part of the “public api”; in practice I’ve found that the only way to avoid both user breakage and maintainer support burden is to restrict what is possible, not to just create a false sense of security by “hiding them better”.

ljharb commented 5 years ago

For an object inspector; I’d argue that that private data should never be displayed, just like you can’t display the closed-over variables that a function uses.

kaizhu256 commented 5 years ago

replying to #issuecomment-449425267

Unintentionally depending on a library's "soft-private" api is easier than you'd think. For example: ... Suddenly my software is not working as expected. How is this possible? It was a minor version change! This library is unpleasant!

that problem shouldn't even exist if you use static-functions instead of class-methods:

// node_modules/library@1.0.0 - using static-functions
var local = {};
local.libraryGetBooks = function (that) {
    return [ /* ... */ ];
};
module.exports.libraryGetBooks = local.libraryGetBooks;

// index.js - using static-functions
var local = {};
local.getBooksByAuthor = function (that, author) {
    return _getBooksWithFilter(that, function (b) {
        return b.author === author;
    });
};
local._getBooksWithFilter = function (that, filterFn) {
    return require("library").libraryGetBooks(that).filter(filterFn);
};

thanks to static-functions, we can safely upgrade to library@1.1.0, even if underscored/"private" members are exported:

// node_modules/library@1.1.0 - using static-functions
var local = {};
local.bookFilterApplyFilter = function (that) {
    return booksFilter(that, /* ... */);
};
local.libraryGetBooks = function (that) {
    return [ /* ... */ ]
};
local._libraryGetBooksWithFilter = function (that, filter) {
    return bookFilterApplyFilter(filter, libraryGetBooks(that));
}
module.exports.bookFilterApplyFilter = local.bookFilterApplyFilter;
module.exports.libraryGetBooks = local.libraryGetBooks;
module.exports._libraryGetBooksWithFilter = local._libraryGetBooksWithFilter;

this proposal overall is misguided, because its trying to fix an anti-pattern (inheritance-based programming) that poorly fits javascript's problem-domain (UX-worflow programming).

ljharb commented 5 years ago

For the umpteenth time, that is not javascript’s sole problem domain, nor is inheritance-based programming an antipattern. Please stop commenting in ways that make broad sweeping statements about the JavaScript ecosystem that cause people to feel excluded, marginalized, and silenced.

zenparsing commented 5 years ago

@ljharb The friction introduced by symbols will certainly discourage the "mucking about" you are concerned with. My hypothesis is that the added friction will address almost all of the pain, at least for 99.9% of the libraries out there. Do we have experience with symbols that might prove or disprove it?

Sure, it's not a "complete" solution. But I think this is a case of "better is worse", because the "better" solution (i.e. private fields) comes with strings attached.

jwalton commented 5 years ago

Oh no! When @ljharb said:

Hard private is not “defeated” if you edit the source - either manually or programmatically.

I almost replied with "Really? Where does it say that in the spec?" But it totally does!

no JS code outside of a class can detect or affect the existence, name, or value of any private field of instances of said class without directly inspecting the class's source

@littledan @ljharb looks like I owe you two a beer next time you're in Ottawa. :)

wrki1qm

Happy holidays, everyone!

bakkot commented 5 years ago

Do we have experience with symbols that might prove or disprove it?

Not directly, that I'm aware of, though for example several members of node (x, x) have said that Symbols are not sufficient. And given their experience I think it's fair for them to assume that people will monkey-patch Symbol-based stuff just as they do with _-prefixed properties (and magic require'd modules and so on).

Java's experience is also relevant. They recently introduced a major feature to the language (modules) in large part because it turned out that "you have to use reflection to get at it", which is what Java had previously meant by private, was very much not sufficient to discourage people from reaching into internals.

gsathya commented 5 years ago

Hard private is not “defeated” if you edit the source - either manually or programmatically. The concern is about at runtime - if the code shipped to the engine has encapsulation, then it will be reserved (just like with variables inside a closure).

This private fields proposal does let you defeat hard private without having to edit the source. It's definitely pretty obscure but possible with the return override trick.

bakkot commented 5 years ago

@gsathya I don't think the return override trick is "defeating" private fields, as people have generally been using the term? It doesn't let you detect or affect their existence or value, it just lets you install them on things.

gsathya commented 5 years ago

@gsathya I don't think the return override trick is "defeating" private fields, as people have generally been using the term? It doesn't let you detect or affect their existence or value, it just lets you install them on things.

Installing them on foreign objects will let you defeat brand checks.. which is exactly what library authors don't want happening, right? Maybe this isn't super useful but I'm just saying this proposal isn't infallible as people seem to suggest.

bakkot commented 5 years ago

Installing them on foreign objects will let you defeat brand checks.. which is exactly what library authors don't want happening, right?

I don't think so? It seems like the main ask is for the class to have exclusive control over (and visibility of) the values of private fields, which remains the case. And the object will still have been run through the class constructor, which allows it to establish any other invariants it likes.

ljharb commented 5 years ago

The brand check is to ensure the brand exists; if you use the return override trick then you correctly pass the brand check by indeed having the brand.

littledan commented 5 years ago

See https://github.com/tc39/proposal-class-fields/issues/179 for discussion about proposals to let a subclass call out to its original superclass (and still use private fields and methods and be extensible). It's hard to see how we could make that sort of integrity the default (even if we went with private symbols) without a bigger break for how subclasses work in JS.

I know a lot of people are ascribing branding as a fundamentally very important thing about private, but I always thought of it as a nice error checking feature, which we are trying to carry forward consistently in the world of private-related features.

I don't think the errors thrown from missing fields or methods are related to the choice of WeakMap semantics--private symbols could also trigger throwing errors more eagerly, and WeakMap-based fields could return undefined for missing private fields. They are just two design decisions that this proposal takes.

hax commented 5 years ago

several members of node (x, x) have said that Symbols are not sufficient.

In fact, the second comment is about "Symbol are not sufficient" for ergonomics (aka. need syntax), not "Symbol are not sufficient" for encapsulation.

hax commented 5 years ago

several members of node (x, x) have said that Symbols are not sufficient.

The first case is very interesting, if you read the whole story, you will find it's a interest conflict between the platform maintainers and library authors. IMHO, the real problem is bad communication, and the issue was solved after having communication. So "hard private" never solve the real problem, it just push the whole responsibility to one side. @jwalton have already showed how babel can be used to workaround hard private, and if authors follow this way, we still have the same risk like today, or even worse, because no one think we need communication any more.

mbrowne commented 5 years ago

Related: https://github.com/tc39/proposal-decorators/issues/191

rdking commented 5 years ago

@jwalton @superamadeus After looking over your comments in this thread, I'm curious to get your opinions on proposal-class-members. You're still going to find the same limited notion of hard private, but the mental model in use is that of a function closure. Let me know what you think.

mbrowne commented 5 years ago

I don't see why we can't have sugar for both hard private AND soft private. IMO decorators are a good solution for soft private, especially if https://github.com/tc39/proposal-decorators/issues/191 can be solved (preferably very soon after the release of the first version of decorators), and also probably the most practical solution since we want decorators for other reasons anyway. But they're not the only option. The important thing is that developers need an ergonomic way to specify either hard private or soft private, as needed. Some developers, particularly some library authors, care more about hard private. Many others really want soft private (and that's what I care most about as well, but I see the option of hard private where desired as a nice bonus). I think it has been well demonstrated that both are important.

kaizhu256 commented 5 years ago

can more people explain how hard/soft privates would fit into big-picture solutions (e.g. UX-workflows)? better understanding real-world javascript painpoints this proposal is meant to solve would provide stronger arguments for its design-proponents.

e.g. what designs of this proposal would practically benefit salesforce's locker service? or help streamline google's security audit of their javascript-products?

rdking commented 5 years ago

What exactly is the use case for soft-private that makes it such a sore point for developers? The only real use-cases I've seen all involve bad practices like monkey-patching and use of pseudo-private API. Unless I'm mistaking something, soft private just allows you a means of discovering the internal details of how a class works, possibly allowing you to modify them.

The reason I don't understand the want is because I have yet to see a single line of external code that tries to or would work better if it could access variables inside a closure. Regardless of the proposal, the core concept for private in ES is (or probably should be) to extend a closure around each instance of a class. In this way, the instance gets its own set of variable that cannot be accessed by closure outsiders.

So, short of using Symbols (which would be my vote for soft private), what's really being requested? Is it just a more ergonomic syntax over Symbols? Or are you really asking for something else?

mbrowne commented 5 years ago

@rdking The semantics of symbols are certainly sufficient for soft private. I think the two best options for providing it in an ergonomic way are either more concise syntax for symbol-keyed properties, or decorators on classes and/or individual private fields. I think this question really boils down to a larger question, "Why do we need reflection on class internals?" There are many use cases for reflection...for some use cases, reflection on public members is sufficient; for others, you need to be able to reflect and even modify private members. The second category is relatively rare, but I provided one example in https://github.com/tc39/proposal-decorators/issues/191.

mbrowne commented 5 years ago

The second category is relatively rare

Just to clarify what I mean by "rare", I mean the average developer might never encounter a need to do this. It's more likely that they would use a library that would use reflection under the hood. For example, if something like breeze.js wanted to make full use of JS classes in a future version for defining entities, the example use case I mentioned in https://github.com/tc39/proposal-decorators/issues/191 might be important. Or maybe some extension to Apollo client or Relay that allowed you to deserialize your GraphQL data to full-fledged objects with fields and methods defined in JS classes (currently such libraries give us the data as plain objects, but no methods). And obviously while only a relatively small number of developers are involved in maintaining those libraries, their usage is anything but rare.

Others could probably give other example use cases for reflection on private fields; this is just the use case I'm most familiar with.

superamadeus commented 5 years ago
This is an off topic comment about a shorthand syntax for symbols. Please disregard. #### Sorry I haven't gotten around to responding much. A lot to take in. @mbrowne > @rdking The semantics of symbols are certainly sufficient for soft private. I think the two best options for providing it in an ergonomic way are either more concise syntax for symbol-keyed properties, or decorators on classes and/or individual private fields. I always pictured there would be a shorthand syntax for symbols. And a way to define context-unique symbols. This would be cool: ```javascript class Example { [:symbolProp] = "hello"; [::classSymbolProp] = "world" } class Test extends Example { [:symbolProp] = "yo"; [::classSymbolProp] = "peeps" } // desugared: class Example { [$_1] = "hello"; [$_2] = "world" } const $_1 = Symbol.for("symbolProp"); const $_2 = Symbol.classSymbolFor(Example, "classSymbolProp"); class Test extends Example { [$_1] = "yo"; [$_3] = "peeps" } const $_3 = Symbol.classSymbolFor(Test, "classSymbolProp"); console.log($_2 === $_3) // true ``` Where `Symbol.classSymbolFor` would resolve common symbols for classes in the same heirarchy.