rdking / proposal-class-members

https://zenparsing.github.io/js-classes-1.1/
7 stars 0 forks source link

What would it mean to drop branding? #10

Open rdking opened 5 years ago

rdking commented 5 years ago

This one is a tough question for me. The reason some people think branding is an issue may look like this:

class X {
  let foo = ~~(Math.random() * 100);
  sum(other) {
    return this::foo + other::foo;
  }
}

The code above throws if other isn't an instance of X. 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:

class X {
  let foo = ~~(Math.random() * 100);
  sum(other) {
    let retval = this::foo;
    try {
      retval += other::foo;
    } catch(e) {/* do nothing */};
    return retval;
  }
}

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 be this, causing a failure on the first line of sum. It should be noted that this is an issue for all but the Symbol.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 each class 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

ljharb commented 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.

mbrowne commented 5 years ago

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.

rdking commented 5 years ago

@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).

rdking commented 5 years ago

@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?

ljharb commented 5 years ago

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.

mbrowne commented 5 years ago

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.

rdking commented 5 years ago

@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:

  1. Let prop be the name of the key being accessed
  2. If prop is an ECMAScript PrivateSymbol, then a. Let newProp be a new Symbol
  3. Else a. Let newProp be prop
  4. Store the relationship between newProp and prop
  5. Call [[Get]](target, newProp, receiver) a. calls to Reflect methods use the relationship to get the original prop for accesses.
  6. Destroy the relationship in step 4.

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.

mbrowne commented 5 years ago

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.

rdking commented 5 years ago

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.

mbrowne commented 5 years ago

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?

rdking commented 5 years ago

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?

ljharb commented 5 years ago

I think you’re forgetting smalltalk when you make claims about how OO was originally conceived.

rdking commented 5 years ago

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.

ljharb commented 5 years ago

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.

rdking commented 5 years ago

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.

ljharb commented 5 years ago

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.

rdking commented 5 years ago

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.

mbrowne commented 5 years ago

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.)

mbrowne commented 5 years ago

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.

ljharb commented 5 years ago

It’s not a property, it’s a field. Properties are public.

rdking commented 5 years ago

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.

ljharb commented 5 years ago

Private fields are like internal slots, conceptually - that's the point.

rdking commented 5 years ago

@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.

rdking commented 5 years ago

@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.

ljharb commented 5 years ago

@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.

mbrowne commented 5 years ago

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.)

rdking commented 5 years ago

@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.

rdking commented 5 years ago

@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

rdking commented 5 years ago

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.

mbrowne commented 5 years ago

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?)

mbrowne commented 5 years ago

...actually never mind; I forgot the point I myself raised about cross-realm brand checks. The existing design of realms really is unfortunate...

mbrowne commented 5 years ago

Maybe this proposal's static private variables could help here? If they could be made to work cross-realm...

rdking commented 5 years ago

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?

hax commented 5 years ago

Current fields proposal do not have cross-realm ability if I understand correctly. So I'm also confused.

hax commented 5 years ago

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...

mbrowne commented 5 years ago

@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.

mbrowne commented 5 years ago

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.

rdking commented 5 years ago

@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.

rdking commented 5 years ago

The issue I'm targeting has 2 points.

  1. Branding should be used to prevent duck typing against private data on this.
  2. Proxy should not need to be modified or unwrapped to implement this privacy without breaking Proxy.
hax commented 5 years ago

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.

ljharb commented 5 years ago

@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.

rdking commented 5 years ago

@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.

hax commented 5 years ago

@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.

ljharb commented 5 years ago

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.

rdking commented 5 years ago

Care to open that discussion on a different thread? It's worth talking about, but not in this thread please.

hax commented 5 years ago

@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.

Igmat commented 5 years ago

@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?

rdking commented 5 years ago

@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.

Igmat commented 5 years ago

@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.

rdking commented 5 years ago

@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