Comments (28)
Initially, in this proposal, let's not build in protected
, abstract
, etc, and leave these to be implemented via decorators and transpilers. Then, with that experience, we can consider adding them in a follow-on proposal.
from proposal-decorators.
Here's an earlier comment of mine on the topic of additional modifiers which I think is pretty relevant to this thread.
from proposal-decorators.
@bakkot, from that comment:
If there is any way at all we can avoid getting into that mess, I would like to do so. It seems far better to me to provide the minimum necessary and then allow user code to define other modifiers to fit their needs, including modifiers which have yet to be invented, and maybe someday provide more after we've gotten some experience in how this bare minimum plays out in JavaScript. I'll stand behind that as good language design any day.
The decorators in the example in friend.js are an attempt to do this in user code, but have a number of issues with leaking private state (primarily the "friend" example), being fairly non-ergonomic to use (a lot of repetitive boilerplate in the "friend" example), or cannot provide certain capabilities (such as overriding an @Protected
method and calling super
to call the superclass version).
Even if we never implement these modifiers, it makes sense to discuss their implications. Understanding how "protected" or "friend" access should work, with or without syntax, helps to direct the actual implementation of private-state in a way that allows user code to emulate the behavior using something like a decorator.
The upshot of syntax is that it gives the engine the opportunity to establish and lock down member access in ways that may not be feasible in user code.
from proposal-decorators.
Here's an example of code using "friend" via decorators: https://gist.github.com/rbuckton/0aebbdc9c723cab0b39c0a0c6684ebb5
Here's the same example using the syntax strawman above: https://gist.github.com/rbuckton/a7736d4c061446df6590c4b5dfb2dccd
from proposal-decorators.
The decorators in the example in friend.js are an attempt to do this in user code, but have a number of issues with leaking private state (primarily the "friend" example),
I'm not sure what you mean w.r.t. the friend
example leaking private state. As to ergonomics and capabilities, I expect the examples could be improved on both fronts - just as an example, the friend
decorator could trivially be implemented such that you could do key(myClassInstance)._x = 2
in place of key.set(myClassInstance, '#x', 2)
. They shouldn't be taken as the best possible implementations, just proofs of concept.
Even if we never implement these modifiers, it makes sense to discuss their implications.
Agreed. I just prefer to frame the discussion as "what sort of accessibility modification do we want to do and how can we ensure decorators are flexible enough to provide that", for the moment.
On that note, a comment on friend
- one nice thing about the example in friend.js
is that it allows exposing only select private properties. It would be easy enough to rewrite it to expose all of them, if that's a use case, but I like the granularity provided by the current design.
from proposal-decorators.
There's a lot of details to work out here, but I want to talk first about the high-level design. I completely agree that we should talk this through.
For protected, I don't quite understand all the problems. Was this only about protected super calls (which I think will be implementable somehow), or are there other issues?
Is there a way for two classes to be mutually friends of each other with your proposal? The FriendKey functionality in friend.js would permit that.
from proposal-decorators.
Do people actually think the general concept offriend
is a good idea? To me the idea of having a unit X
with private internal state accessible only to itself and thing F
has always been a code smell. Why not either put that state in a Map
or somewhere where both can access it? Or just make it public and don't expose X
to consumers.
I feel like the reason no languages have nailed these accessibility modifiers is that they are usually workarounds to problems inherent in OOP, not first class language design concepts. JavaScript has excellent, intuitive modules and closures that alleviate the need for these modifiers.
As for abstract
, I think protocols can fill this void while providing a great primitive that can be used in FP, too.
from proposal-decorators.
I do not think the concept of friend
is a good idea; particularly because there's no way besides closures to safely grant access to a desired friend, without also granting access to anything else.
Similarly, I agree that protocols seem like they obviate any use cases for abstract classes - is there a use case I'm missing?
from proposal-decorators.
Just a thought: friends should be designed in a way that they can be used both ways between two classes. For example, in a subject observable, the subject needs to access the private subscriber to invoke its method, but that shouldn't be exposed publicly. Similarly, the private subscriber needs to access the subject's private listener list to remove itself on unsubscribe
.
Also, JSDOM probably has a blue million different cases where it needs to access private data from another object, and IIRC they currently use weak maps extensively for that reason.
from proposal-decorators.
I do not think the concept of friend is a good idea; particularly because there's no way besides closures to safely grant access to a desired friend, without also granting access to anything else.
Or at least in the sense that's being proposed here. It appears the design of friend.js (define a scope outside the class) would suffice to fix that, so if you instead did something like this, it might work better (as a bonus, this could be exportable as a binding):
Edit: The error is early (parse or module instantiation time) when defined incorrectly, and friends may only have their methods defined in the same function-level scope as they're defined or imported. Also, exported friend bindings don't show up in dynamic imports, and it's a load error if an exported friend isn't defined as a member by the dynamically loaded module or its dependents.
// It's an early error at parse or module instantiation time if `#x` or `#y` are
// defined multiple times, or if either variable is defined with the incorrect
// member type.
friend scope { #x, protected #y }
class B {
friend {#x, #y} from scope;
getX(obj) {
return obj.#x;
}
setX(obj, value) {
obj.#x = value;
}
callY(obj) {
return obj.#y();
}
}
class A {
friend {#x, #y} from scope;
#x;
protected #y() { return this.#x + 1; }
constructor(x) { this.#x = x; }
}
Similarly, I agree that protocols seem like they obviate any use cases for abstract classes - is there a use case I'm missing?
I think it's just that abstract classes are easier to subclass, but also, you can't define instance state for protocols, and even if you could, you can't initialize them with arguments without putting them near feature-parity with classes (which would make them no longer protocols, but multiple-inheritance classes instead). And if you were to go the latter route, you might as well just support multiple inheritance with normal classes - it'd be easier and cleaner.
from proposal-decorators.
As for clarification, I chose to include protected
in the friend descriptor to make it easier for the engine to write the correct bytecode (it doesn't have to wait for the eventual class definition to make the correct call type). That part could change, though, and I'm not terribly attached to it. (If it happens to be feasible to omit it, I'd be happy to do it.)
from proposal-decorators.
@isiahmeadows Interesting idea. We could also accomplish something similar with "rebind decorators", see #17 . However, I'm not pursuing that right now, because it seems like enough is possible with the current proposal, e.g., see friends.js.
from proposal-decorators.
That'd work, too. Mine would allow for better engine optimization, but if all you're looking for is pure structure, that'd be sufficient (and slightly more concise).
from proposal-decorators.
@isiahmeadows I don't understand how yours would lead to better engine optimization.
from proposal-decorators.
Just a thought: friends should be designed in a way that they can be used both ways between two classes.
One option is to define an intermediary:
// friends.js
class AB {
static getX(a) { return a.A#x; }
static getY(b) { return b.B#y; }
}
export class A friend AB {
#x = 1;
getY(b) { return AB.getY(b); }
}
export class B friend AB {
#y = 2;
getX(a) { return AB.getX(a); }
}
// main.js
import { A, B } from "./friends";
const a = new A();
const b = new B();
a.getY(b); // 2
b.getX(a); // 1
Another option for friend
would be to hold onto the binding of the friend (rather than the value) and only evaluate the binding when checking access:
// friends.js
export class A friend B {
#x = 1;
getY(b) { return b.B#y; }
}
export class B friend A {
#y = 2;
getX(a) { return a.A#y; }
}
// main.js
import { A, B } from "./friends";
const a = new A();
const b = new B();
a.getY(b); // 2
b.getX(a); // 1
from proposal-decorators.
I agree that if friend
only operates in a single scope, that it works fine.
However, what’s the utility if it means you can’t split your classes up into separate modules? Can you help me understand the use case where you need more than one class to share private data, and these classes are small enough that cramming them into the same file is practical and maintainable?
from proposal-decorators.
With the proposed syntax you can also split across files:
// a.js
import { B } from "./b";
class A friend B {
#x;
}
// b.js
import { A } from "./a";
class B {
getX(a) { return a.A#x; }
}
However, this introduces circularity. While circular dependencies are permitted with ES modules, they are a possible source of subtle errors around evaluation order.
from proposal-decorators.
Ah, right. While in my projects i intend to lint against circularity, that would indeed allow friends to be useful across modules. Thanks for clarifying!
from proposal-decorators.
I wonder if it would make sense to propose a mechanism to provide a hint to the loader about dependency order, something like:
// a.js
import defer { B } from "./b";
// creates the binding for `B`, but prefer b.js to load later.
// possibly an early error/TDZ to access `B` before a.js module body has finished evaluation.
// To avoid TDZ, `friend` would need to capture the binding for `B` but not read the value
// until access checks are made.
class A friend B {
#x;
}
// b.js
import { A } from "./a";
class B {
getX(a) { return a.A#x; }
}
With import defer
, "b.js" would be given a lower priority than "a.js", allowing "a.js" to evaluate first. If both "a.js" and "b.js" were to use import defer
, they both have the same priority and imports resolve as per usual, but with the added effect of the early error/TDZ for any use of an import before the module body finishes evaluation.
from proposal-decorators.
@concavelenz was suggesting something similar to this "import defer" thing.
from proposal-decorators.
@rbuckton I'm still having trouble understanding the advantages of these modifiers being built-in as opposed to using decorators.
Is this a categorical thing, that for something as core as access modifiers, it's inappropriate to put it in a library? Or that the syntax is nicer without an @
, and friend element access is more ergonomic as x.A#b
than key.get(x, A, "b")
?
One thing that makes me a bit worried about adding access modifiers at this point is that they are complicated, detailed, and the proposal differs from what we have now (e.g., these are somewhat tighter than what TypeScript provides). A lot of the complexity is in protected
, but many programmers have been giving the feedback that friend
is more important.
The upshot of syntax is that it gives the engine the opportunity to establish and lock down member access in ways that may not be feasible in user code.
You mentioned stronger guarantees, but I don't quite understand. What wouldn't be feasible in user code? Is this about the distribution of the FriendKey?
The protected modifier allows a subclass to access all of the private names of a superclass that were declared as part of a protected member.
Protected increases the dynamism of private names significantly. In particular, the lookup of an identifier #x
becomes no longer lexical but instead there are other sources to consult (super-classes, friends) in a certain order.
One option is to define an intermediary:
For friends, a very common case is that class A is allowed to see the private elements of class B, and A is defined after B. For example, this comes up when class A is test code accessing the internals of class B. It would be pretty awkward to define an intermediate class AB as you're suggesting above, as opposed to using a @testable
decorator which just exposes everything for metaprogramming.
Another option for friend would be to hold onto the binding of the friend (rather than the value) and only evaluate the binding when checking access:
Seems like this only works if they're in the same scope. For example, if I have a class, and then tests in another file, I'm not sure how to use this mechanism.
from proposal-decorators.
from proposal-decorators.
Yes, but this should still be statically discernable in practically every case.
I'm not really sure how to do that for the protected proposal above; maybe you can explain more. One detail here is that the superclass is mutable.
This is why I created my
friend
proposal the way I did - it was for this very reason.
Where can I find your friend proposal?
from proposal-decorators.
@littledan As long as you make sure private is hard-private and clearly non-overridable, you have to make protected
members expose themselves differently, and engines can use that information to statically determine the difference between protected and private. In particular, C++ operates this way for the most part. For example:
class Foo {
protected #bar() {}
}
class Bar extends Foo {
method() {
// Engines can detect this case statically to not be private, because
// it's not defined in this scope.
#bar();
}
}
class Baz extends Foo {
// Engines can detect this case statically to not be private, because it's
// marked with `protected`.
protected #bar() {}
}
class Qux extends Foo {
protected { #bar }
method() {
// Engines can detect this case statically to not be private, because
// it's marked as `protected`.
#bar();
}
}
If you don't require protected
, then you're now back to soft-private, but with all the inheritance concerns as well. (That, I would expect to be a no-go.)
Where can I find your friend proposal?
I was referring to this comment of mine. I was replying via email, or I would've linked it from there.
from proposal-decorators.
@isiahmeadows
I think it's just that abstract classes are easier to subclass, but also, you can't define instance state for protocols
I have various concerns about the actual need for protected in the presence of private and an exposing constructor pattern for now. I would like to defer the idea of adding it until we get more experience in user land to see if private and exposing constructors are sufficient. It would keep the line clear about what can access properties. In particular I am concerned about:
class A {protected #foo}
class B extends A {}
const b = new B;
class C extends b.constructor {
get foo() {return #foo;}
}
Object.getOwnPropertyDescriptor(C.prototype, foo).get.call(b);
Being a method to circumvent protected to an extent that it is effectively public.
I fear that friendly faces the same problem of getting a hold of a privileged class and extending it.
from proposal-decorators.
I fear that friendly faces the same problem of getting a hold of a privileged class and extending it.
If friends are tied to scopes rather than classes, that problem mostly disappears. But I do agree that protected
still carries a risk.
from proposal-decorators.
@bmeck are you concerned about friends as done through decorators for that privacy risk?
from proposal-decorators.
This seems to be less relevant to this proposal or possible extensions, and should instead be its own proposal related moreso to fields/private methods. I'm going to close this, but feel free to reopen if you disagree.
from proposal-decorators.
Related Issues (20)
- Stage 3 allow decorators to generate PJOs HOT 1
- Timeline / proposal for extensions? HOT 8
- Using accessors makes code less clean and readable HOT 21
- Counterintuative ordering when using `accessor` in combination with `init` methods HOT 37
- Can't dynamically set private field in initializer HOT 17
- Context.private doesn't seem to support group accessor HOT 1
- Support changing the backing field used by auto-accessors HOT 2
- Towards stage 4 HOT 14
- Field and Accessor initializers should run after the field/accessor has been defined HOT 12
- Stage 3 cannot access the class for static method decorator HOT 3
- Idea: syntax for decorator composition. HOT 3
- How to exclude methods from class decorator HOT 2
- Add target class to the context HOT 6
- Order of execution HOT 3
- It isn't clear when `addInitializer` functions are called on class decorator HOT 2
- feature request (separate proposal?): `context.addPostInitializer()` HOT 18
- Readme text and types are outdated against actual spec HOT 2
- Field decorator initializer should support configurable field HOT 8
- Was there a purpose for non-lexical ordering of decorator applications? HOT 2
- Write upgrade guide for previous iterations to Stage 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from proposal-decorators.