Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope-based privacy #44

Open
dead-claudia opened this issue Apr 16, 2018 · 2 comments
Open

Scope-based privacy #44

dead-claudia opened this issue Apr 16, 2018 · 2 comments

Comments

@dead-claudia
Copy link

dead-claudia commented Apr 16, 2018

Cross-posted from tc39/proposal-class-fields#94 with edits for relevance


Edit: Inner classes and shadowed properties already exist in the class fields proposal. Updated to reflect this.

Repo/explainer: https://github.com/isiahmeadows/private-data-proposal

Since this whole "Classes 1.1" is about thinking from a clean slate, I thought I'd throw another thing at the wall, to see if it'd stick any.

The basic concept for my idea is to use scopes (for a very loose meaning of the term) as the basis of privacy rather than classes. It's mostly compatible with this proposal on the surface, but it avoids most of the gotchas and doesn't require explicit support for protected/friend/etc. The extensibility was pretty easy to spec, and the only complicating factor was making private fields work with subclasses.

Here's some of the benefits it offers:

  • It works with object literals as well as classes. You can do C-style "structs" without having to expose too much of that.
  • It gives you friend access for free just by moving a declaration up a scope and/or exporting a function that reads/writes it.
  • You can actually export private names trivially through exporting accessor/mutator functions, so they're accessible to e.g. subclasses, friend classes, or just other functions that happen to need access to a field or two.
  • You can use this with dynamic imports and namespace imports, just by using the namespace itself. I had to add a separate statement for this, but it was relatively easy to spec.
  • The proposal turned out far simpler and easier to explain/spec than I expected. It's slightly larger than this in scope (mainly due to extra syntax rules, but the runtime roughly equal mod scope checks), but it covers most of the immediate extensibility complaints I've found so far, and it's much more consistent with fewer edge cases. (It took me maybe a few hours in its entirety to get 90% of the details locked down, and another 2 for a little bit of polish.)
    • It helps having a mental model that can be explained with two short paragraphs and a code block. If you read nothing else, that serves as a great summary of what it's supposed to be like. There are few places with curly braces where my private can't be used:
      1. Object destructuring assignment patterns
      2. Named import sections
      3. Named export sections

And yes, it's fully feature-compatible (and mostly syntactically compatible) with the existing class fields proposal, even though it's syntactically and semantically wildly different from its core:

  • Class-local fields exist. You just need to drop a private #x in the class.
  • If you just do #x = 1/#x: 1, #x() {}, etc., it's implicitly declared for free if you didn't already declare it outside of the class/object. (This is where 99% of the syntax compatibility comes from.)
  • Inner classes and scopes can inherit access to private fields defined in outer classes.
  • You don't need an arrow function to access a private field. Being within the declaration's scope is enough.

Notably absent is this: the ability to guard outside access to certain fields to specific classes/scopes/modules. The proper way to handle this is just consenting-adults style: if they say it's private, just consider it private. Unless it's a password or something similarly sensitive (which should be clearly not exported at all), there's no need to truly care too much about it. If it's "protected" or "friend", the subclass or friend class could just choose to expose what you let it see anyways.

There are some drawbacks:

  • No clear way to limit exposure of certain fields to specific classes/scopes/modules beyond that of hard privacy. Unlike C++, which has friend classes, this only has hard privacy (you don't expose it) and firm privacy (you do expose it). You can't control any finer-grained than that.
    • There's nothing preventing a subclass or "friend" class from exposing this directly anyways, so I wasn't convinced there needed to be a way to prevent this. Heck, even C++'s PIMPL idiom doesn't prevent you from accessing the pointed-to's inner struct via assembly unless it decides to use processor guards to prevent you from dereferencing the pointer (which almost never happens outside kernel user APIs). So it's at least partially justified by making the "consenting adults" clear.
    • If it's truly something sensitive like a crypto key you're protecting from hardware and malicious scripts, you shouldn't even be exporting the key in the first place.
  • That freaking sigil...if getting rid of a sigil wasn't so freaking difficult, I would leap onto that like white on rice.
  • The declaration style isn't super JS-y. I'd love to figure out a way to make it a little more so, but JS has historically been so lax on scopes that pre-ES6, I failed to understand why the language even had standalone block statements in the first place short of grouping, and even in that case, comments and functions could do a better job than blocks. (We at least have let now, but other features like do expressions in the pipeline are starting to make meaningful use of curly braces more.)
@bathos
Copy link

bathos commented Apr 16, 2018

This seems to nearly retain the flexibility of current WeakMap solutions and can be reasoned about in the same terms, which I like very much.

One thing I don’t understand: what is the advantage of constraining use of private members to objects which were created in a scope that originally had access to them? While I don’t think there’s any great utility lost on account of that, it’s the one point where it’s less flexible than WeakMap, and it seems like it would add runtime overhead (the need for tracking what was available to each object) without an obvious benefit (it’s no less private if this constraint doesn’t exist). Do I have it backwards maybe? Is this behavior easier for engines to optimize or something?

@dead-claudia
Copy link
Author

@bathos

This seems to nearly retain the flexibility of current WeakMap solutions and can be reasoned about in the same terms, which I like very much.

It also happens to cover the 99% use case for weak maps, and the transpiled equivalent trivially desugars.

The remaining 1% is when you need to expose that weak map to other modules, but 99% of the time there, you don't actually need to care about the presence of the object in that map, just the data associated with it. (This comes up frequently in things like JSDOM.) In my experience, the only times you really need weak maps for anything this doesn't work for is when you need to add expando private data to an object you didn't create.

One thing I don’t understand: what is the advantage of constraining use of private members to objects which were created in a scope that originally had access to them? While I don’t think there’s any great utility lost on account of that, it’s the one point where it’s less flexible than WeakMap, [...]

The advantage is that when you restrict it to in-scope object allocations, you gain the ability to know exactly how many slots to allocate for that object. Then it becomes a matter of tracking a static, per-AST key + offset list, which makes it flat out trivial for an engine to calculate optimal layout for such objects. (In fact, most statically typed languages operate this way, and it's why C/C++ have such well-defined object layouts.)

Keep in mind, the class-oriented proposals just allow private fields to be scoped to classes, it amounts to about 90% of the work anyways.

[...] and it seems like it would add runtime overhead (the need for tracking what was available to each object) without an obvious benefit (it’s no less private if this constraint doesn’t exist). Do I have it backwards maybe? Is this behavior easier for engines to optimize or something?

You already have to track that with all the existing proposals, the difference is that it's only checking one scope, where mine needs to check potentially multiple. It's marginally harder for engines to optimize, but only marginally (and they'll do it regardless). Probably the bigger difference is how they would implement each one:

  • With the existing proposals, they'll need to create a new object subtype that admits a private field list, or at least create a nullable pointer to that field list if they choose to just stick it on the base object type.
  • With my proposal, they can just modify the base object type, so it's easier to fuse into their existing object models, but they will have to adjust their scope handling a bit more.

(It's still a pretty cheap check either way, and ICs are fully capable of eliminating it or at least dodging the memory access worst case scenario, in both cases. Also, the rest of the optimization process is roughly the same.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants