Open rdking opened 5 years ago
Again, this isn’t just an issue with Proxy - all the builtins (except Array) that have internal slots will cause some prototype methods to throw when .call-ed on them. Putting “pre-existing” in quotes doesn’t change that it does, in fact, exist already.
Would this object itself be exotic? Would it be sealed, but with all of its fields writable? What happens when that object is passed around? This seems like it would add a lot of complexity over the current proposal.
I thought one of the reasons for branding was for cases where it's possible that duck typing could give misleading results. If it weren't for cross-realm issues I would say this could just be implemented in user-land and isn't something the language needs to provide, but without the ability to create a cross-realm constant I can understand why existing solutions are considered unsatisfactory.
@ljharb I'm confused. Did I not make it clear already that the combination of Proxy and anything using private slots is a "pre-existing" problem? I put it in quotes because the simple fact that there's a hole in your path doesn't require you to fall in it if you know its there. Yet that's what class-fields is doing. I don't wish to discuss that subject further as it is not the point of this thread.
Please direct your comments toward the actual issue being presented. That's why I tagged you in. You have a perspective that is valuable to me (especially because its an educated opinion that differs from my own).
@mbrowne Can you help me understand how branding affects cross-realm issues? I already get the idea that if an object in another realm, it's prototype will be a different object from it's match in the current realm. What I don't get is why branding is of any benefit in that case. Can you give an example of how duck typing leads to a problem?
Branding allows nominal typing, which is different than structural (duck) typing.
If you want nominal typing, you can’t rely on a public shape/interface, nor can you rely on instanceof
.
Yes, exactly what @ljharb said. I believe the reason for the term "duck typing" in the first place comes from the idea, "if it quacks like a duck, then it's a duck". But what if it's a person making quacking noises? The goal is to create a mechanism for determining what class an instance belongs to that can't be fooled.
@ljharb
Would this object itself be exotic?
I don't think it would need to be but I'm open to other arguments.
Would it be sealed, but with all of its fields writable?
This is what I'm currently anticipating.
What happens when that object is passed around?
It can't be passed around. The only access to its properties is via operator ::
. Basically, operator ::
would work as follows:
prop
be the name of the key being accessedprop
is an ECMAScript PrivateSymbol, then
a. Let newProp
be a new SymbolnewProp
be prop
newProp
and prop
prop
for accesses.This seems like it would add a lot of complexity over the current proposal.
That's true. Compared to the current class-members, this does introduce a bit of complexity. However, Proxy needs to be able to work regardless of whether or not there are private fields. The proper solution would be to allow Proxy to unilaterally tunnel private slots that are not [[ProxyHandler]] and [[ProxyTarget]]. But I hope the fact that this isn't already the case means there was a very good reason for not doing so from the beginning.
Here's a more academic perspective, for what it's worth...
As far as the early object-oriented thinkers were concerned, objects should be like black boxes—total encapsulation of instances. Alan Kay defined objects as being like mini-computers sending messages to each other on a very fast network.*
So from that perspective, brand checking is technically a violation of encapsulation. It would probably be better if there were a way to accomplish it without breaking encapsulation of instances, but in practice I don't think this is a real concern. I think a more proper solution would need to be built into the language from the beginning, i.e. I think it's too late to avoid cross-instance access for JS.
* This is a metaphor, but interestingly the model can be applied to real network communication as well, with microservices behaving like an object graph. Incidentally there has been some recent exploration in this area—applying the actor model to microservices.
Yes, exactly what @ljharb said. I believe the reason for the term "duck typing" in the first place comes from the idea, "if it quacks like a duck, then it's a duck".
I think the expression is: "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck." So a human making duck-like quacks doesn't work. What if it's a duck in a thin, clear plastic suit that doesn't prevent the duck from doing things? Does that still qualify as a duck? That's the Proxy case. With branding, unless we do something, we won't be able to recognize the duck just because it's wearing a suit. Poor Donald.
So from that perspective, brand checking is technically a violation of encapsulation.
That would be true if we were working with the original model of object-oriented, where all properties of an object were always what we're calling "own properties". Prototype-based OO is like having a computer that, when it doesn't know the answer, consults another computer close at hand and returns whatever that computer said as its own answer. This wasn't within the considerations of the original OO thinkers.
I think the expression is: "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck." So a human making duck-like quacks doesn't work.
You're right of course. I realize it wasn't a flawless analogy but I think you got the point ;-)
Prototype-based OO is like having a computer that, when it doesn't know the answer, consults another computer close at hand and returns whatever that computer said as its own answer.
What do prototypes have to do with private instance variables?
I don't recall saying that prototypes had anything to do with private instance variables at all. What I said is that the concept of OO as originally conceived didn't include the possibility of a prototype-based language. Please don't get distracted.
Are there any problems with the solution that I've presented above? Are there any issues I need to consider?
I think you’re forgetting smalltalk when you make claims about how OO was originally conceived.
Likely, but even Smalltalk didn't fully reflect the original concept of OO. It was colored heavily by the developers' then-present understanding of general programming.
In either case, what I'm looking for is an understanding of what you think about my attempt to create Proxy-safe branding.
To me, branding is a requirement, and as I’ve said, “proxy-safe” imo is something that should be solved for all language values, and until then, none.
While I understand your opinion on the issue of "proxy-safe", that means little to nothing to developers who want to be able to use both private data and Proxy on the same object. TC39 failing to implement internal slot tunneling is what prevents proxy safety. So if we avoid using internal slots to implement private, we can avoid the pothole in the road. This doesn't require modifying Proxy at all.
So, since there is no modification to how Proxy works, the goal of fixing Proxy can be put off until the TC39 members who don't want Proxy to tunnel internal slots either leave the board or change their minds. As long as Proxy isn't being modified, there's nothing wrong with making private data proxy safe. Certainly you can agree to this much.
It’s not about internal slots being the implementation of private, it’s about the conceptual precedent they set.
No, i do not agree - i think it would be wrong to add tunneling for private fields when internal slots did not tunnel. iow, imo no matter how they are specified or described or implemented, they should both behave the same wrt Proxy.
Sorry, but that seems illogical to me. If the private container is a property of the object (even if the name is hidden), then there is no reason that it should behave like a slot. Further, not interfering with Proxy ensures private data will remain the usefully non-interfering thing we want it to be.
This approach doesn't require Proxy to unwrap at any time other than where it currently does. That means we're not "tunneling" anything. That's the point of this idea. Skip the thought of tunneling altogether.
I don't recall saying that prototypes had anything to do with private instance variables at all.
Ok, I'll put it a different way: I don't think prototypes should have anything to do with brand checking. I'm not sure how relevant that is.
Here's a separate question that's probably more relevant: should a brand check return true for both instances and proxies wrapping instances? What if you want to be sure that you're dealing with the original class and not a proxy? (I'm not sure if that would be an important distinction in practice, I'm just raising the issue for discussion.)
Just realized that my statement was probably confusing:
I don't think prototypes should have anything to do with brand checking.
I was referring only to the mechanism of doing the brand checking. Obviously the whole point is to validate the class and attached prototype. Anyway let's set this aside; sorry for distracting on this point.
It’s not a property, it’s a field. Properties are public.
To be clear: prototypes have absolutely nothing to do with either the mechanism behind brand checking or this idea.
should a brand check return true for both instances and proxies wrapping instances?
Are Daffy, Donald, Daisy, Hewey, Dewey, Louie, Scrooge, and Launchpad still ducks even though they wear clothes? Proxy, while not intended to be 100% transparent, are still intended to be mostly translucent. Unless the behavior of the object involves internal slots, it should just work when Proxy-wrapped.
What if you want to be sure that you're dealing with the original class and not a proxy?
If you want to do that, then in the class
constructor, store the value of this
as private data.
var assert = require("assert");
class X {
let self = null;
let noProxy = () => { assert(this === this::self); }
constructor() {
this::self = this;
}
someFunction() {
this::noProxy();
//Other actions
}
}
So it's definitely possible to be sure you're not being proxied. It's impossible to spoof the value of this
in the constructor from the outside. Only a base class can do that. But if a base class sends you a Proxy as this
, then your this
really is a Proxy, in which case it doesn't matter.
Private fields are like internal slots, conceptually - that's the point.
@ljharb
It’s not a property, it’s a field. Properties are public.
I get what you're thinking but you going down a road I don't intend to follow. ES currently has no concept of what a "field" is. This proposal doesn't include the concepts contained in class-fields, and that "instance property declared in the class
definition" would be referred to as an "instance property" under this proposal. Please think in terms of this proposal when considering the Idea I present above.
@ljharb
Private fields are like internal slots, conceptually - that's the point.
In this proposal, private properties have very little in common with internal slots. They are regular properties of a container object (per this idea). That's my point. Leave "fields" in the discussion of "class fields" as they don't belong here. That's why I hid both your last comment and this one.
@rdking not sure why you hid the very on-topic comments - private instance data is not acceptable unless it behaves like an internal slot. Regular properties of a magically private container object stored on the instance, to me, is a much more complicated mental model that simply isn't worth exploring.
I deleted my previous comment. @rdking I realized afterwards that when you said "instance property", you were talking specifically about the idea described in this issue description, not just the existing class-members proposal as-is. So then, I gather that this new concept of a private instance property would replace the currently-documented concept of instance variables, is that correct? (For some reason I previously thought you meant to introduce this as an additional feature, i.e. "in addition to" not "instead of" instance variables.)
@mbrowne
So then, I gather that this new concept of a private instance property would replace the currently-documented concept of instance variables, is that correct?
Exactly. This would be a replacement of the core mechanism for this proposal.
@ljharb The only reason I didn't hide your latest comment is because you related it back (albeit poorly) to the topic at hand.
Regular properties of a magically private container object stored on the instance, to me, is a much more complicated mental model that simply isn't worth exploring.
You are just as free to feel that way as you are to exit the discussion. I would feel somewhat disheartened to find you so inflexible though. "Magically private"? Not at all. Re-read the description in the OP. Private by means of a private Symbol as discussed by @zenparsing. The difference is in how the private Symbol is being used. Where he wanted to make it a publicly create-able key type like Symbol, I see it as an implementation detail to remove the private container from public accessibility
As for issues of the mental model, this may actually be even less complicated than the original model of this proposal. The idea is that private members are just properties of an object that is itself a property of the owning object. Essentially, the model is this: obj.[[private]].private_member
. That's a simple model. The only thing that needs to be remembered is that since you can't know what [[private]]
is, the only way to access the private members is: obj::private_member
.
If that's complicated, then ES may already be too hard of a language to learn.
I just realized something...isn't it already possible to use WeakMaps for brand checking? It's nice that you offered duck typing as a workaround, but (1) it's not equivalent to nominal typing (as we discussed) and (2) maybe WeakMaps are already sufficient. (Convenience is nice and all, but is it worth it in this case?)
...actually never mind; I forgot the point I myself raised about cross-realm brand checks. The existing design of realms really is unfortunate...
Maybe this proposal's static private variables could help here? If they could be made to work cross-realm...
I'm lost as to what goal you seek to achieve. Are you trying to have a brand signature that can successfully identify objects across realms?
Current fields proposal do not have cross-realm ability if I understand correctly. So I'm also confused.
Actually I think in theory private symbol may have possibility to get cross-realm brand, for example let the same url module generate same private symbol (use some algorithm which use module url as seed).
It's very impossible with PrivateName (weakmap). I can't imagine how to merge weakmaps for different realm...
@rdking Yes. I don't know enough about the internals to know if it's actually possible; it was just an idea I had. Example:
class MyClass {
static const instances = new WeakMap()
// (constructor omitted)
static isMyClass(obj) {
return instances.has(obj)
}
}
But I guess there's no way to make that work cross-realm.
And I suppose this is out of scope for an ECMAScript standard and rather more of a browser thing, and would have security implications...so forget it. I initially thought this was on-topic but after thinking about it more, I don't think it would help with the issues @rdking is trying to solve.
@mbrowne You're right. It's a bit beyond the scope of the issues I'm targeting, but that's not to say it can't be done. If a seeded pseudo-random number generator were used as the source for the Private Symbol such that given the same seed, the same sequence would be generated, it would be possible. ES and Browsers would have to work together on that to make that happen.
The issue I'm targeting has 2 points.
this
.And I suppose this is out of scope for an ECMAScript standard and rather more of a browser thing, and would have security implications...so forget it.
@mbrowne
I think it's hard to say what should be in ES spec and what should be in Web Standards at first. For example, Promise was developed as the DOM standard at first. Another example, AbortController
is in the DOM, but we may still need language mechanism as https://github.com/tc39/proposal-cancellation .
About fields proposal, many programmers don't understand why hard private. I think hard private have valid use cases, but the problem is why it should be solved in language spec? Why not browser side? For example, make some reflection APIs respect same origin policy. I'm not saying it's easy or it's better or it's can cover all use cases, but I guess such solution would be much more easy to understand and use for most programmers.
@hax browsers aren't the only engine; it needs to be available in the language, and it's got nothing to do with same origin - it's not about mistrusting other realms, it's about mistrusting all other scopes.
@hax The general idea is that library vendors don't want developers mucking around with implementation details so they can be free to change how the code works internally without affecting their users. Hard private (like closures) is the only real way to do that. Any soft-private implementation will find developers using it to modify or call internal API that may be subject to removal.
Frankly, I think developers who do that should shoulder the burden when their code breaks instead of blaming it on the developer of the updated library. At the same time, I think that certain library vendors need to stop being so sloppy with their versioning. In the end, I can't fault the requirement for hard-private. It's what I want too.
@ljharb @rdking
I fully understand the motivation of hard private. What I'm trying to say is there should be the balance between the library authors and users. There are reasons why users want to hack the libraries. We only have the voice of some library authors but little voice from the users. (Though we already got some from the objectors of current class fields proposal, but many TC39 guys assume this is because they don't like #
and just treat them as invalid.)
I hope we can have a solid motivations and requirements list, for example, separate the security requirements and the engineering requirements. I mention same origin policy because it's a security model which can satisfy security requirements. If hard private have nothing to do with security requirements, then let's forget about it. But if hard private is only about engineering requirements from the library authors, you TC39 should also consider the engineering requirements (use reflection to hack it) from the library users.
Users don't have to hack a library at runtime - they can fork it, which is a better practice anyways since it makes the changes more robust and explicit.
Separately, users already can't hack the bodies or closed-over variables of functions, and this is a very critical feature for library authors. Private class fields merely adds that missing feature to class instances.
Care to open that discussion on a different thread? It's worth talking about, but not in this thread please.
@ljharb Yes they can fork it, they can patch it in postinstall, they even can monkey patch it in browsers via loader hook/service worker, etc. The only difference is, you can't blame the author if you do such thing. Actually many programmers think you also can't blame the author if you access _foo
, access private foo
in TS. This is why they (include many library authors) think hard private is unnecessary. Of coz some users still blame authors even they did such thing (though I never see anyone rely on Object.getOwnPropertySymbols(o)[0]
dare to blame the author). If you look the problem deeply as this perspective, you will find hard private is just a weapon of some library authors to push back the blames, no real technical reason.
@rdking Sorry, I will stop discussing it in this thread. This is my last comment about hard private.
@ljharb
To me, branding is a requirement,
Cool, for you it's a requirement. Don't you think that putting feature (brand-checking) that could be easily implemented in user-land (~20 lines of code) into language and MIXING it with another feature (encapsulation) is kinda weird? Especially when first ISN'T required by majority of developers while second (encapsulation) IS?
and as I’ve said, “proxy-safe” imo is something that should be solved for all language values, and until then, none.
Ok, we have [[Internal Slot]]
that cause problems and you propose to make MORE problems with privates, and solve them later in unknown way - awesome.
Don't you think that providing solution that doesn't have such problems (e.g. Symbol.private
) and further rework of problematic language part (e.g. rewrite [[Internal Slot]]
with Symbol.private
) would be much easier?
@Igmat Unless we can somehow convince TC39 to fix Proxy by allowing it to tunnel internal slots (as it arguably should have from the beginning), or add detect (isProxy) and unwrap to Proxy, there won't realistically ever be a solution that solves it for all language values. I can only assume that there was some good reasons for crippling Proxy they way they did.
On the other side, I think of branding as a requirement as well. Try considering the only truly private construct in ES currently: the closure. Each run of a function produces one, effectively an instance. Its just the functions lexical environment record. There's no way to run a function exported from a closure against a duck-typed closure, or for that matter, any other closure at all. They're intrinsically bound.
Now given that we're trying to add into ES a way to produce the same kind of closure-like privacy on an object (via class
), branding is simply the means by which we ensure that our target functions cannot be executed against a foreign pseudo-closure. Branding reproduces that intrinsic binding.
The point of this thread is to produce a method by which this intrinsic binding won't interfere with the operation of Proxy. With this proposal I originally introduced a modification to proxy by means of a new operator and corresponding operation that would not be visible to Proxy. Apparently @ljharb believes that approach wouldn't fly with TC39.
However, both class-fields and private-symbols introduce a new key type, requiring that Proxy be modified as a result. Well, it might also be said that class-fields introduces a new operator .#
and a new operation that attempts to access a slot on the object, avoiding proxy operations altogether and immediately causing the incompatibility.
While I don't see why class-fields would intentionally choose to paint itself into a corner like that, the fact that private-symbols is even being considered by TC39 means that the approach it offers, including the modification of Proxy, has not been deemed unacceptable. I'm simply thinking to use that same mechanism to hide a pseudo-closure on the object. This would allow Proxy to continue working when the target carries private data. Branding would no longer be a problem.
Now, if you have any other technical problems with branding, I'd like to hear them. If you think there's something about the approach I'm considering that won't work, I'd like to hear that too.
@rdking, it seems that our problem is misunderstood of brand-check
meaning.
For what I've seen/read so far from committee brand-check
- is a check for that some object was constructed by particular constructor and wasn't wrapped in any means. So:
class A {
}
class B {
}
const a = new A();
const b = new B();
const proxiedA = new Proxy(a, {});
assert(brandCheck(A, a) === true);
assert(brandCheck(A, b) === false);
assert(brandCheck(A, proxiedA) === false);
So, since a !== proxiedA
in all cases, then brandCheck
for A
should be false on proxiedA
. And I always use this term in such meaning.
This means that tunneling privates through proxy is opposite to brand-checking all privates.
From other side, yours brand-check
is different as far as I can tell, and works like this:
assert(brandCheck(A, a) === true);
assert(brandCheck(A, b) === false);
assert(brandCheck(A, proxiedA) === true);
Correct me, if I get your point wrong. But if I'm right, I don't mind such brand-checks, even if they would be default (even though I still think, that majority of developers won't need even such light brand-check
), but I guess that's a deal-breaker for committee (or, at least, some its members like @ljharb), since it doesn't give you an opportunity to preserve initial class invariants.
@ljharb, please correct me, if I misunderstood your meaning of brand-check
term.
@Igmat You've got the right idea for how I see brand-checks. There's nothing useful about my brand being able to tell the difference between me and me wearing a Fitbit. It merely needs to be able to identify "me", regardless of what I'm wearing. As I said before, branding is a protection to ensure that a function cannot be called against an invalid pseudo-closure. Trying to extend its use beyond this causes unnecessary problems.
Btw, being able to spot when you've been proxied is easy to do even now. Trying to use branding for this purpose is just overkill.
const self = Symbol("Self");
class X {
/* //class-members syntax
* let self = this;
*
* //class-fields syntax
* #self = this;
*
* //current day syntax
*/
constructor() {
this[self] = this;
}
isProxied() {
return (Object.getPrototypeOf(this[self]) === Object.getPrototypeOf(this)) &&
(this[self] !== this);
}
}
var a = new X;
var b = new Proxy(a, {});
a.isProxied(); //false
b.isProxied(); //true
This one is a tough question for me. The reason some people think branding is an issue may look like this:
The code above throws if
other
isn't an instance ofX
. That's not really a problem. If the developer intends to allow the function to accept duck typed objects, then it could easily be re-written as such:and everything would work fine. The real problem is Proxy. Since Proxy doesn't tunnel internal slots, a Proxied instance of
X
would look no different than a duck typed object, except that the Proxy would bethis
, causing a failure on the first line ofsum
. It should be noted that this is an issue for all but theSymbol.private
approach, which keeps private data as properties of the instance object.Here are my initial thoughts on this matter.
I like branding, as I don't see a good reason to ever expect that a non-instance will ever have any of the private fields declared by X. At the same time, Proxy is a ridiculously big issue that class-fields brushes aside as a "pre-existing" issue. This is not something I'm willing to do as Proxies are an extremely useful tool even if I'm not trying to create a Membrane.
What if there was a single, guaranteed, non-writable, non-configurable, non-enumerable, non-reflecting property that always exists on every object with an object as its value. Using this as the private container (as opposed to a closure) would guarantee that Proxy could continue to work in its currently (imo)crippled state without interfering at all with this proposal. Access would still be through operator
::
. The key here is that every unique "class" would have it's own "private Symbol" name for that property. This means eachclass
would have a "private Symbol" name for the private property that would be shared by its instances. Object literals, however, would each have a distinct "private Symbol" of their own, only accessible by functions defined in the literal's declaration.You might think of this as an integration of private Symbols and this proposal to remove the Proxy limitation without needing to remove branding. There's no reason why the 2 concepts can't exist together. It all just works if we make private Symbols an implementation detail for this proposal. Since the private Symbol for the container is referenced by operator
::
, and operator::
is only valid within the lexical scope of the Object/class declaration, it's impossible to share the name. But because it's a property reference, Proxy doesn't cause an issue, and we get what we want.Done this way, we get to keep branding, but since the check just looks at a named property of the object, we get private without losing Proxy.
Side notes
Other capabilities with this approach include:
So what are your thoughts?
@hax @Igmat @mbrowne @ljharb @shannon @zenparsing @trusktr