-
Notifications
You must be signed in to change notification settings - Fork 113
The concept of "access mode" or "reference mode" #195
Comments
Nested properties using one of the suggested syntaxes from above: // foo is a public property, also instance of this.constructor:
this.foo.publicMethod()
this.foo['public'+'Method']()
this.foo##.privateMethod()
this.foo##['private'+'Method']()
this.foo#.protectedMethod()
this.foo#['protected'+'Method']()
// foo is a protected property, also instance of this.constructor:
this#.foo.publicMethod()
this#.foo['public'+'Method']()
this#.foo##.privateMethod()
this#.foo##['private'+'Method']()
this#.foo#.protectedMethod()
this#.foo#['protected'+'Method']()
// foo is a private property, also instance of this.constructor:
this##.foo.publicMethod()
this##.foo['public'+'Method']()
this##.foo##.privateMethod()
this##.foo##['private'+'Method']()
this##.foo#.protectedMethod()
this##.foo#['protected'+'Method']() Neste properties using the proposal syntax (plus string access): // foo is a public property, also instance of this.constructor:
this.foo.publicMethod()
this.foo['public'+'Method']()
this.foo.##privateMethod()
this.foo[##'private'+'Method']()
this.foo.#protectedMethod()
this.foo[#'protected'+'Method']()
// foo is a protected property, also instance of this.constructor:
this.#foo.publicMethod()
this.#foo['public'+'Method']()
this.#foo.##privateMethod()
this.#foo[##'private'+'Method']()
this.#foo.#protectedMethod()
this.#foo[#'protected'+'Method']()
// foo is a private property, also instance of this.constructor:
this.##foo.publicMethod()
this.##foo['public'+'Method']()
this.##foo.##privateMethod()
this.##foo[##'private'+'Method']()
this.##foo.#protectedMethod()
this.##foo[#'protected'+'Method']() |
Personally I like For example, suppose that // some-module.js
// a "module private" or "module protected" pattern
const privates = new WeakMap
export class Foo {
##foo = 'foo'
constructor() {
privates.set(this, this##)
}
}
// another class in the same module
export class Bar {
constructor() {
let f = new Foo
console.log(privates.get(f).foo)
console.log(privates.get(f)['f'+'oo'])
}
} import Bar from './some-module'
new Bar
// Output:
// "foo"
// "foo" Notice that because we use the The engine would need the new concept of an "access mode" associated with a single reference. Some things (among other things) we'd have to consider: class Obj {
foo = 1
#bar = 2
##baz = 3
test() {
// same instance
console.assert(this instanceof Obj)
console.assert(this# instanceof Obj)
console.assert(this## instanceof Obj)
// they're all the same object, just that the references are in different "access modes":
console.assert(this === this)
console.assert(this === this#)
console.assert(this === this##)
// enumeration, etc, is based on access mode:
console.log(Object.keys(this)) // ["foo"]
console.log(Object.keys(this#)) // ["bar"]
console.log(Object.keys(this##)) // ["baz"]
// get descriptors, like we already know! Access mode filters them internally
console.log(Object.getOwnPropertyDescriptors(this)) // { foo: { ... } }
console.log(Object.getOwnPropertyDescriptors(this#)) // { bar: { ... } }
console.log(Object.getOwnPropertyDescriptors(this##)) // { baz: { ... } }
// set descriptors, like we already know!
Object.setOwnPropertyDescriptor(this, 'foo', {...})
Object.setOwnPropertyDescriptor(this#, 'bar', {...})
Object.setOwnPropertyDescriptor(this##, 'baz', {...})
// use JavaScript like we already know!
}
}
let o = new Obj
o.test() I think it is not just easy to reason about it this way, but also just easy to work this syntax with all of today's existing tools. class Foo {
// creates a accessor descriptor with get/set, just like normal.
// Only access mode determines whether or not you are allowed to interact with
// the foo property, which is tangential to how descriptors work.
get #foo() { ... }
set #foo() { ... }
##bar = "bar"
test() {
// access mode is tangential to concepts we already know:
console.log(typeof this) // "object"
console.log(typeof this#) // "object"
console.log(typeof this##) // "object"
console.log('bar: ', this##.bar) // bar: bar
}
}
const f = new Foo
// We'd may have to bikeshed some things, but it is totally doable:
// the same as in the methods? Seems to make sense. They're the same object.
console.log(typeof f) // object
console.log(typeof f#) // object
console.log(typeof f##) // object
f.foo = 1 // works
f#.foo = 2 // Throws an Error, something like "can not gain protected access outside of a class"?
f##.foo = 2 // Throws an Error, something like "can not gain private access outside of a class"?
// ^ those errors do not leak information about which properties exist.
class Bar extends Foo {
test() {
console.log(typeof this) // object
console.log(typeof this#) // object
console.log(typeof this##) // object
this.foo = 'foo' // it works, new public prop
this#.foo = 'foo' // it works, inherited protected setter used
this##.foo = 'foo' // it works, and is a private property in Bar scope
super.test()
this##.bar = 'lorem'
console.log('bar: ', this##.bar) // bar: lorem
}
} Notice the output with respect to
because there is one bar in each private scope (hard privacy in that we can not detect private variable by detecting errors setting them, so setting private variables always works, and sets them in the current class scope). Doing it this way becomes about "permission to access protected or private scope depending on which symbol is used on an object reference". Something else to bike shed: Sense muuuuuuch it makes, me thinks! |
We'd have to get into more details eventually, but from a top level perspective I like that with this approach I can work with objects just like before. import _ as 'lodash'
class Foo {
##foo = 3
##bar = 4
test() {
console.log(_.pick(this, [ 'bar' ])) // { bar: 4 }
_.assign(this##, { baz: 'baz' })
console.log(this##.baz) // "baz"
}
} This also means opportunities to inspect access mode. // ...
let ref = this
console.log(Reflect.accessMode(ref)) // "public"
ref = this#
console.log(Reflect.accessMode(ref)) // "protected"
ref = this##
console.log(Reflect.accessMode(ref)) // "private" Access mode also dictates access mode along the prototype chain during lookup, so:
Maybe the engine needs to internally mark each property as "protected" or "private", to look it up during access. Public props don't need any internal value. But although I've no experience even looking at engine code, this "access mode" idea doesn't seem too hard. |
A special case: class Foo {
constructor() {
let s = super# // Uncaught SyntaxError: 'super#' keyword unexpected here
}
} |
As I’ve stated on other issues; i don’t think the concept of “access levels” is appropriate for javascript. “protected” is a poor choice of name, since it doesn’t actually protect anything (since anything can get access to protected members by temporarily subclassing, and extracting methods). There’s no way in JS I’m aware of to robustly share access at any future time (ie, to later-created subclasses) without using lexical scope - so I’m not clear on what problem language support for “protected” would solve. It’s clear that syntax space can be created for it - but not that there’s value in doing so. See #86 for example (your own issue) and many similar issues/comments on this repo. |
I think this question is answered both by the several previous threads where you raised this issue, the decorator future path and by @ljharb's post. |
that's not what protected is for. If I give you a reference, you can't call the methods from public space. that's what protected is for. I'm not as much concerned about people grabbing the method source. |
I'm confused; a function has the same behavior no matter where it's invoked from. |
I just described. ^
Yeah there is. If I can imagine it, others can too!
It's not hard to imagine... |
I believe there will be strong opposition to ever making a first-class function's call behave differently based on where it's invoked (as opposed to where/how it's defined). |
yes, but protected/private access will only work on this class with the statically created hierachry slot mentioned in my previous comment (i.e. created during class definition and given to objects at [[construct]]) It's like |
@ljharb I just want to mention again because you have stated this before.
The final keyword would be used on the class to prevent further subclassing. |
edited typos in my previous comment |
@shannon thanks, that's a useful clarification - so how would a parent class that provides "protected" data ensure that child classes (that extend the parent long after the parent is finished evaluating) are @trusktr i see, so you're saying i couldn't extract a protected method from the bottom of one chain and .call it on the bottom of another chain? |
plus what Shannon said, and I've also mentioned in the "other threads" how to implement |
It wouldn't. As a developer I would only export final classes. Protected would be internal to library/module. I could be mistaken, but I believe that is the point of protected. To avoid redundancy, and allow you to keep some shared data between subclasses without exposing it to your public API. |
they don't, only the child classes define if they are final. This is convenience for library authors.
Right, .call and .apply won't change the internal classes that the methods operate on. There could be additional checks in place to throw meaningful errors, f.e. maybe methods using the sigils can only be .called or .applied on objects that match the internal hierarchy. Additionally the scope where the .call and .apply happen can be checked, just like regular method calls, and errors thrown when the scope is wrong. or etc |
accessing |
@shannon mentioned
additionally if we can add internal mechanics to save these references in their access modes to variables ( |
@ljharb here is a rather simple example: class Vehicle {
#velocity = { x: 0, y: 0 };
#position = { x: 0, y: 0 };
protected #wheels = 0;
protected #topspeed = 10;
update(acceleratorDown, wheelRotation) {
// caculate new #velocity based on user input
// clamp velocity to #topspeed
// caculate new #position based on #velocity
}
}
class Car extends Vehicle {
#wheels = 4;
}
class SlowCar extends Car {
#topspeed = 1;
}
class FastCar extends Car {
#topspeed = 100;
}
export default final class PlayerCar extends Car {};
export final class PlayerSlowCar extends SlowCar {};
export final class PlayerFastCar extends FastCar {}; We don't want to expose these values to be edited by the player but we do want to keep the code DRY and allow for declarative subclassing. This is almost certainly a desirable thing to do and I don't see why it wouldn't fit within JavaScript. So I'm really not sure why you keep stating that there is no value in doing this. The alternative is closed over variables or weak maps and then we are just right back where we started. |
I don't know if other languages do this but to me it would be ideal if final didn't prevent subclassing completely, but just ended the chain for protected members. in other words any further subclassing didn't inherit the protected members but it wouldn't error. I don't know how others feel about that though. |
Wouldn't you still be able to do
Even if the exported constructors are final, the parent constructors are not. They'd have to be made final too somehow, after all of your 3 subclasses have been created. |
@loganfsmyth Well, I would expect that using the final keyword would walk the prototype chain and set a final parent constructor. So |
@shannon Wouldn't that mean
would make
would then fail because it was extending a final class? |
@loganfsmyth not quite what I meant. It would just set the parent constructor of PlayerCar to a final version of Car. It wouldn't retroactively go and and change all the other classes to final. Edit: I meant to say PlayerCar in the case of your first example. PlayerSlowCar would have a parent constructor of a final version of SlowCar. |
@loganfsmyth I think you meant There's always some solution that can be imagined:
|
Shannon's idea seems perfect; a "final version". Just like the ideas here with (I'm in mobile, sorry for typos!) |
@shannon If we aren't mutating the existing class, what does making a final version of a class entail? We'd have to have a full copy of the prototype object and all the methods with the new
is
Nope, the prototype of the class constructor is the parent class. The |
@rdking, well said about protected! What are your thoughts on the "access mode" and "reference mode" ideas specifically?
Ah, right! So modifying the Anywho, the discussion of |
That wouldn't work with the above idea where I mentioned that with APIs like Sidenote, @littledan, I think you closed this issue too early because I haven't raised this idea before in another thread ("access mode" and "reference mode" ideas), and it might seem as if your opinion matters more than a multitude of community member opinions when an issue is closed quickly, generally speaking. It would be nice for it to be open (and bonus if we could somehow get JS community members more involved in the discussions). How can we garner more community involvement? |
@loganfsmyth Oh wait, I forgot that I mentioned that "access mode" applies to prototypes too, so A class internally can write I think this should work because the If the public code changes the prototype of an instance object, well then that's another story (and the same rules apply as with |
@trusktr I've been trying to figure out how to word this properly. If I'm not careful, I might find myself in a contradiction...
Reference mode and access mode aren't good concepts. In this sense, I agree with @ljharb. They don't really fit the language. The problem is one of scopes. When you think of modes, you think of something that can be altered, just like "Night Shift" on my Macbook is a mode that the display can take to ease eye strain during late hours. Instead of thinking in that way, try reversing the picture. When code outside of any class looks at a class instance, it should only see the public API. When code inside a subclass looks at an instance of the class or a subclass of one of its base classes, it should only see the common protected API (which includes the instance's public API). When code inside any class looks at an instance of itself, it should see the entire private API (which includes the protected API of all bases). It's just like dealing with a nested function's closure. let x = 1; //Public
function A() {
let y = 2; //Protected
function B() {
let z = 3; //Private
return {
printFromB() {
try {
console.log(`x = ${x}`);
} catch(e) {
console.log(`"x" is out of scope`);
}
try {
console.log(`y = ${y}`);
} catch(e) {
console.log(`"y" is out of scope`);
}
try {
console.log(`z = ${z}`);
} catch(e) {
console.log(`"z" is out of scope`);
}
}
};
}
return Object.assign(B(), {
printFromA() {
try {
console.log(`x = ${x}`);
} catch(e) {
console.log(`"x" is out of scope`);
}
try {
console.log(`y = ${y}`);
} catch(e) {
console.log(`"y" is out of scope`);
}
try {
console.log(`z = ${z}`);
} catch(e) {
console.log(`"z" is out of scope`);
}
}
});
}
let retval = Object.assign(A(), {
printFromGlobal() {
try {
console.log(`x = ${x}`);
} catch(e) {
console.log(`"x" is out of scope`);
}
try {
console.log(`y = ${y}`);
} catch(e) {
console.log(`"y" is out of scope`);
}
try {
console.log(`z = ${z}`);
} catch(e) {
console.log(`"z" is out of scope`);
}
}
});
retval.printFromGlobal();
retval.printFromA();
retval.printFromB(); From this perspective, "protected" is like a means of sharing the scope of function A with select other functions but not with Global, and without sharing the function B. Can you see how this perspective is different from that of "access modes"? |
@trusktr I forgot to address something. The reason What makes it more perplexing is that from an access perspective, either you can access it or you can't. No matter what scope you're in, that should be true. If you don't maintain that kind of perspective, you'll create inconsistencies in the mental model. That's why I said you need to think of it in terms of scopes. "What can you access from a given scope?" is the question to ask. Right now, your "access modes" seem to ask "What can you access with a given context?". The problem with that is that the answer changes depending on your scope! For instance, is it at all safe to use |
@rdking I largely agree with your comments in #195 (comment) - however, what thhat effectively means is that both the protected API and the public API need to follow semver (ie, distinguish between breakage and non-breakage), since external consumers could be relying on either one - which effectively makes both APIs identically public. What is the benefit of language-level support for what amounts to a second simultaneous public API? (when one could be similarly conveyed by any number of conventions, either with prefixes, symbols, etc) |
Agreed.
Please don't conflate the two surfaces like that. Sure, by some means other than directly in the lexical scope of the donor class, they are indeed both accessible. However, the scope from which they are accessible is a critical difference.
I stated it before, didn't I? When compared with the Protected promotes cooperation between library developers and users by giving them well-defined points of modification. It also makes it easier for developers to add or remove such points. This is not something you get from the existing conventions. |
I can see now why no one was understanding what I meant. It doesn't make much sense because of circular logic. I'm starting to wonder if the final keyword could just sever the prototype chain all together. And create a fresh base class that just wraps calls to the parent constructor and public methods and prevents external code from reaching the parent. You wouldn't be able to use instanceof to compare to the parent but that would be expected if it was defined this way. Having said all that, first class private symbols makes this problem much simpler. Share the private symbols where needed and don't expose them in your public API and this makes the protected keyword unnecessary. |
@shannon how would you suggest sharing anything (beyond lexical scope, of course) that doesn’t cause it to be fully public to everything? |
@ljharb well in my example it could easily just be lexical scope as these would likely be in the same file. But if you are leading to the issue about sending the symbols cross file/module then I think you would have to rely on a future proposal to do this securely. However, in practice, you really don't need to worry about this extra security. During your development you can trust that the modules you import are valid and will not be middleman replaced to capture the private symbols. For production you will most likely be bundling your application so again you can trust that the modules you import are valid. Then the only way to capture keys would be a source modification. And as you have stated many times, this out of the scope of the encapsulation proposed. |
Production includes “node” which typically does not bundle; anything that can be imported anywhere can generally be imported anywhere - if a feature that attempts to allow privileged sharing is language-level, it’s important to me that, in practice, you can rely on only privileged observers can access the shared values. |
@ljharb Well then I would argue that replacing a file module is exactly equivalent to modifying source through babel or other means. You would want the base class to be importing the sublcass to share symbols, not the other way around. So the fact that the base class can be imported anywhere doesn't come into play here. The only way to capture symbols would be to modify what |
I’m not talking about source modification; I’m talking about importing any specifier/file path at runtime and thus getting access to the “protected” data. Base classes can’t know about all subclasses (spread across modules) without creating circular dependencies; are you suggesting that’d be the only way to make it work? |
@shannon Wouldn't that require the base class to import every possible subclass? That's completely backward, and impossible. It would require the subclass to exist before the base class is ever declared. Besides, there are ways to pass the symbols along during construction. Suppose the base class checks to see if |
@rdking i don’t believe there’s any way the superclass can run code after the subclass constructor’s super call - ie, no way to enforce that the subclass has deleted the property. It seems like both a bug farm and a lot of boilerplate to require every subclass to add |
@ljharb I agree, it's a lot of boilerplate. I'm just stating that there is a method for doing so. Also, yes, the base class can indeed enforce the deletion. All that's required is that the 2 keyed properties be factory-produced getters. The getters could capture the value to be returned, delete itself, then return the result. Fairly simple stuff. |
That's still easily done without any language support. Just define a class that leaks a bunch of private symbols, then use those private symbols in the other classes. Not much to that. |
@rdking that works if the subclass accesses the property; however, if it forgets, then the value would remain as a public property on the instance, which could be used to affect the parent class’s behavior for that instance, without the subclass intending that exposure. iow, the superclass author wouldn’t be in charge of access control for that info. As to that class that leaks private symbols, anyone could import it, so that’s also making things fully public. |
True enough. I don't usually need to account for sloppy programmers, so I sometimes forget they exist.
Not true here. The leaky class doesn't get exported. So it can't be imported from anywhere. |
ah, you mean doing all of that in lexical scope. That certainly works, but with that restriction you don’t need private symbols, you can use weak maps, lenses, and all sorts of patterns. Language-level access sharing would need to work (and restrict access) across modules imo to carry its own weight. |
Hence, "protected" support. This private symbol sharing is just that, a means of providing for "protected". The only problem is that, like you've noticed, without language support it's both sloppy and can be done by other means nearly equivalently. Putting some kind of protected support in the engine means that the engine itself cleanly handles the transfer, and ensures that access remains restricted until some class spills the beans. |
Yea that was my point about private symbols making the problem much simpler. I agree with you that language support for protected is required to work around these other issues. (and I agree with @ljharb's line of questioning around it) Sorry if I brought this conversation off topic. |
@trusktr I've been thinking further, and there's 1 other issue with your approach. |
Nope, it's an error in that case. Though it could be possible to have an extended syntax for applying visibility helpers that are assigned to variables. In gave a syntax idea of it (f.e. search for
These access modes are not dependent on that property at all. Everything needed for it to work is tracked and associated to a constructor during class definition time, and tampering with the prototype after the fact wouldn't break it (just like it is currently impossible to change [[HomeObject]] of a In other words, just like there is currently no way to change the value of [[HomeObject]] inside a concise or class method, there is no way to modify what
|
(NOTE This first post was originally about having both private and protected features with symmetrical syntax, but the next comments evolved into the concept of "access mode" and "reference mode")
Personally I like the more poetic/verbose/meaningfully-clear
private
/protected
keywords, and my editor's autocompletion is fast enough (if my typing speed can't beat it).But if we're going to stick with the
#
sigil, can we at least figure howprotected
will be allowed the possibility of entering the picture later (if it ever does), without it being asymmetrical? (f.e.#
for private andprotected
for protected wouldn't be symmetrical)What about having both
#
and##
?I feel like
#
for protected and##
for private would be nice. The longer one with more symbols is for private becauseprivate
is more hidden thanprotected
.Personally I would be fine losing the
.
symbol when accessing protected or private:For dynamic string access, and alternative could be the
#
symbols go outside:or
But that last one is the most awkward. If we want to preserve the notion of accessing props on an object with
.
, then another possibility is:which seems to be clear:
this
,this#
, andthis##
are all treated like objects, and the.
and[]
patterns stay the same as they have always been.Other objects:
Is it possible to leave space in the spec for
protected
, in case it is added later?The text was updated successfully, but these errors were encountered: