Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

What would it mean to drop branding? #10

Open
rdking opened this issue Dec 9, 2018 · 113 comments
Open

What would it mean to drop branding? #10

rdking opened this issue Dec 9, 2018 · 113 comments

Comments

@rdking
Copy link
Owner

rdking commented Dec 9, 2018

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:

  • Protected can be implemented using the private container of the prototype object.
  • Friend can be implemented by passing the private container name to the befriended object/class.
  • I haven't worked out details for something like "internal" yet.
  • Functions could have static and private static lexical variables.

So what are your thoughts?


@hax @Igmat @mbrowne @ljharb @shannon @zenparsing @trusktr

@ljharb
Copy link

ljharb commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

@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
Copy link
Owner Author

rdking commented Dec 9, 2018

@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
Copy link

ljharb commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

@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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

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
Copy link

ljharb commented Dec 9, 2018

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

@rdking
Copy link
Owner Author

rdking commented Dec 9, 2018

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
Copy link

ljharb commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

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
Copy link

ljharb commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link

ljharb commented Dec 9, 2018

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

@rdking
Copy link
Owner Author

rdking commented Dec 9, 2018

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

This comment has been minimized.

@rdking
Copy link
Owner Author

rdking commented Dec 9, 2018

@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

This comment has been minimized.

@ljharb
Copy link

ljharb commented Dec 9, 2018

@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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link
Owner Author

rdking commented Dec 9, 2018

@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
Copy link
Owner Author

rdking commented Dec 9, 2018

@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
Copy link
Owner Author

rdking commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 9, 2018

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
Copy link

mbrowne commented Dec 12, 2018

One other thing: JS currently has both regular properties and symbols. The current mental model (or at least my current mental model) is that both of these belong directly to objects, even though the reflection methods group them separately. Introducing a new container concept for private members would probably cause confusion...so are public members in a container too, or are they at the top level and only privates are in a container? Are symbols their own container? etc.

But I suspect that all of this has more to do with how you would choose to explain the proposal than the spec of the proposal itself. I suspect that there would be no problem going with the idea in the OP and that it could be explained either way, for better or worse.

@ljharb
Copy link

ljharb commented Dec 12, 2018

@mbrowne duplicate names can't be prevented, because if you choose #foo, and someone sticks a foo property on your instance, and you have code that reflects on the instance and dynamically looks up values, you'd need to be able to get to the public property value.

@mbrowne
Copy link

mbrowne commented Dec 12, 2018

@ljharb I'm not following. When I first brought this up in class-fields, all the technical arguments given against duplicate names depended on the hard privacy requirement, i.e. those arguments would no longer hold without it. Suppose for the sake of argument that private members were always soft private (meaning available to some core reflection API), with no way to make them hard private. In that case there could be a policy ensuring it wouldn't be possible to "stick a foo property on your instance" in the first place if the instance already had a private field #foo (or, more on-topic for this thread and the class-members proposal, a private property foo)—it would be an error.

@rdking
Copy link
Owner Author

rdking commented Dec 12, 2018

@mbrowne In the presence of private data, the mental model for objects changes, regardless of the proposal. In general, objects will appear to have 2 distinct property bags. Both bags equally contain properties of the object. The difference is that while the object is willing to share one bag, the other bag is held in secret.

@mbrowne
Copy link

mbrowne commented Dec 12, 2018

In general, objects will appear to have 2 distinct property bags.

Do you believe this is necessarily true for any OO language with private instance state, or only for JS? "2 distinct property bags" implies a structure, e.g.:

- object
  - private property bag
     - foo
  - public property bag
     - bar
     - baz

That's not my mental model of Java or C#, and I doubt that thinking about it that way is very common. But now that I think about it, C++ syntax probably encourages such a mental model, so perhaps it depends in part on experience with / exposure to C++.

@rdking
Copy link
Owner Author

rdking commented Dec 12, 2018

Only for JS. I'm not particularly worried about the mental model of other languages. My only concerns are to find a way to map the concept of data privacy into ES in a way that:

  • achieves the desired functionality
  • feels like regular ES code
  • is maximally compatible with all existing language features
  • is minimally conflicting with other, non-competing proposals
  • is maximally extensible for future related concepts
  • requires as few trade-offs as possible
  • preserves the "prototype-based" nature of the language,
  • leaves adequate room to ensure that what is sugar has a reasonably sugar-free equivalent

For any ES proposal I provide, these are the primary constraints I follow. I rail as hard as I do against class-fields because it violates all but the 1st and 4th of these constraints. If I were a TC39 member, I would have openly vetoed this proposal reaching stage 3 on these grounds.

I get how much work has been put into it, and I understand the pressure the board is under to release some version of private. However, there should be some minimum agreed-upon standard for quality of a proposal. Given that there are members of the board who are still vocal about the deficiencies of that proposal, its obvious that the minimum standard has not been met, even if it hasn't been clearly defined.

@rdking
Copy link
Owner Author

rdking commented Dec 12, 2018

As a point of reference, the suggestions I made in the OP brings me closer to the 3rd and 4th constraint.

  • Branding that only ensures the existence of the private data is one of the minimum requirements for the 1st constraint.
  • Resolving issues with Proxy wrapping without removing branding gives me the 3rd constraint.
  • Making the private members properties of an object makes them compatible with decorators and gives me the 4th constraint.

As for the remainder of the proposal:

  • The 2nd constraint is satisfied
    • by using let/const for declaring private members
    • by ensuring everything that is not a public part of the prototype or constructor
  • The 5th constraint is satisfied because a keyword can be used to add protected & friend support
  • The 6th because no trade-offs have been identified yet, and the ones from the competing proposals have been avoided.
  • The 7th because the prototype is not being unnecessarily avoided.
  • The 8th because both WeakMap and Symbol can be used to reproduce most of the effect of this proposal. Also, the implementation would be such that simply adding new syntax would be enough to enable factory functions to produce identically featured objects.

@Igmat
Copy link

Igmat commented Dec 12, 2018

@Igmat by not throwing (ie, no brand check), it makes it much more likely that a developer will unintentionally conflate “undefined” with “has no symbol” - the exception intentionally forces yo unto be very explicit when you don’t want to brand check.

@ljharb, since strict brand-check (which filters out proxy wrappers) isn't something common and highly required by majority of developers and in same time ruins some patterns for usage of Proxy, it seems that developer has to be explicit when he/she DO WANT to brand check.

@ljharb
Copy link

ljharb commented Dec 12, 2018

The default should be a balance of what’s most common, but also what’s safest. In this case, the safety of a brand check by default (and the danger of lacking one by default) imo is the most important. It’s also useful to note that part of the reason these checks aren’t common is likely because they’re not ergonomic to achieve.

@Igmat
Copy link

Igmat commented Dec 12, 2018

@ljharb,

  1. What danger of lacking brand-check are you talking about?
  2. Why does existing brand-check implementation isn't ergonomic?
    Decorator for brand-check will take ~20 lines of code and could be published to npm, so you even don't have to copy-paste it to each project. Just use it like this:
    import { branded } from 'brand-check';
    
    @branded
    class A {
        method() {
                // method won't be invoked unless `this` is actual A instance (not proxied)
        }
    }
    Should I create such package, so you stop saying that brand-checking isn't ergonomic and has to be put into language directly? IMO, the only reason, why there is no such package yet, is that it's actually NOT NEEDED by majority of developers.

Have you ever though that your use-cases could be less common for other developers?

@rdking
Copy link
Owner Author

rdking commented Dec 12, 2018

@ljharb When you refer to "brand-checking", do you mean only the strict, WeakMap/WeakSet-like check that is capable of determining whether or not the object is distinctly a product of the constructor? Or does your meaning afford a slightly weaker variant that is merely capable of ensuring that the needed private members are in existence?

@rdking
Copy link
Owner Author

rdking commented Dec 12, 2018

BTW: To everyone, please review the updated README.md as it exists in the "private-symbol-approach" branch of this repository.

@ljharb
Copy link

ljharb commented Dec 13, 2018

@rdking it's a bit of both; the primary goal is ensuring that the needed private members are present, the secondary assurance is that it means that the constructor, and the superclass chain's constructors, have all ran as part of setting up the initial instance.

@rdking
Copy link
Owner Author

rdking commented Dec 13, 2018

@ljharb So, unless I missed something, it really is only about ensuring that the target object has all the necessary required members. Since the only initialization that cannot be done outside the constructor is the staging of the private members, then branding is only meant to ensure that all required members (even from base classes) are present.

Would you say that's a fair summary? If so, then why has there been chatter about the notion of branding being more strict than this? It's made my understanding of the subject less clear.

@mbrowne
Copy link

mbrowne commented Dec 13, 2018

@rdking I just re-read this whole thread and I don't understand why it's necessary to create a new container object for the private properties. Why couldn't you use a separate private symbol for each property, and keep everything at the top level of the instance? So an object would simply be a collection of slots with the same structure as it currently has, with the only difference being that in addition to regular properties and public symbols, keys could also be private symbols—just as in the private symbols proposal (unless I'm misunderstanding something about how that proposal solves the proxy issue). I think that would be a much simpler model that would also adhere more closely to JS's underlying object model as inspired by the Self language—what you've often referred to as JS's prototypal heritage, that you want to remain true to.

@mbrowne
Copy link

mbrowne commented Dec 13, 2018

Regarding brand checking, I also want to say that I was mixed up about what was being discussed when I made some of my earlier comments on this thread (as was probably obvious). In case anyone else is not crystal clear on it, first of all it's important to note that "brand checks" have previously been discussed in different contexts. As someone who hasn't been following the most recent discussions on brand checking in class-fields, it took me until about halfway through this thread to realize that you were only discussing what happens natively, not anything related (at least not directly) to user-land brand checks. I went back and looked at an earlier discussion about this; here's a quote from @ljharb:

It's not just nice to have; being able to expose an "isArray"-like method that brand checks is critical.

@ljharb, I assumed at the time that you meant that literally (although given the different usage more recently of the term "brand checking" now I'm less sure)...I guess something like the following (using class-fields syntax since that was the original context):

(Brand-checking example 1)

    (() => {
        const BRAND = Symbol('MyClass')
        class MyClass {
            #brand = BRAND
            static isMyClass(obj) {
                return obj.#brand === BRAND
            }
        }
        return MyClass
    })();

It can alternatively be implemented using a WeakMap (a downside is that this increases memory usage of course):

(Brand-checking example 2)

    (() => {
        const instances = new WeakSet()
        class MyClass {
            constructor() {
                instances.add(this)
            }
            static isMyClass(obj) {
                return instances.has(obj)
            }
        }
        return MyClass
    })();

In more recent discussions, the usage of the term has been quite different. As @Igmat put it in tc39/proposal-class-fields#134:

Nobody can apply my methods to something that isn't instance of my class if those methods use privates (somewhere it was mentioned as brand-check)

I now realize (and hopefully my understanding is correct) that this is the only meaning @rdking had in mind in the OP of this thread, i.e. ensuring that the following code would throw an error:

class A {
  let x

  foo(obj) {
    obj::x;
  }
}

class B {}

const a = new A()
a.foo(new B())  // error

A somewhat related topic is binary methods, for example:

class User {
  let socialSecurityNum
  ...
  isSamePerson(user) {
    return user::socialSecurityNum === this::socialSecurityNum
  }
}

Note how my "Brand-checking example 1" above relies on this same ability to access private members of other instances of the same class. This is why some of my earlier comments in this thread talked about that. And I suppose that the increased memory usage in "Brand-checking example 2" was the reason for @ljharb's comment (from the much earlier thread) that isArray()-like brand-checking methods "can't be done in user land easily/performantly/elegantly otherwise".

I'm not bringing all of this up to re-discuss all of it (and I realize this is a long post) but rather to try to be very precise about what we're discussing, because it can get quite confusing especially when you bring proxies into the mix. I think @rdking's updated readme in the "private-symbol-approach" branch is probably sufficiently clear about the intended meaning of "brand checking", but I would just like to remind everyone that it's always helpful to clarify the context when discussing this, especially for the sake of people who might have heard the term "brand checking" before and already have past associations with the term.

But I would like to establish for certain whether or not the cross-instance private member access in "Brand-checking example 1" is seen as a requirement for '"isArray"-like methods that brand check', even if it's not directly relevant to this thread, because it's certainly still relevant to this proposal.

@mbrowne
Copy link

mbrowne commented Dec 13, 2018

And while I'm writing long comments, I might as well write them all at once, so I just wanted to point out one more thing which is that although a suit is probably still a good analogy for a proxy, it's important to recognize that their are scenarios in which the different identity matters. Just to give one of many possible examples:

const someSet = new Set()
class MyClass {}
const obj = new MyClass()
const proxy = new Proxy(obj, {})
someSet.add(obj)
someSet.has(obj) // true
someSet.has(proxy) // false

Personally I think of a proxy as just a classic example of delegation, just supported by the language rather than done manually. The proxy receives messages (property access or method calls) and delegates them (or not) to the original object as it sees fit. Yes, it's accurate to think of the proxy as a wrapper, but it's still two separate (albeit closely related) objects communicating with each other. But as I said, I still think the spacesuit is a pretty good analogy overall.

@Igmat
Copy link

Igmat commented Dec 13, 2018

@mbrowne, could you please clarify your position.
Is DEFAULT and IMPLICIT brand-check has to filter out proxies?

@mbrowne
Copy link

mbrowne commented Dec 13, 2018

@Igmat

could you please clarify your position.
Is DEFAULT and IMPLICIT brand-check has to filter out proxies?

Ultimately I don't think brand-checking should filter out proxies, no (although I might be open to reconsidering my opinion if there were some convincing argument I haven't seen yet). In other words, in the long term, I agree that the proxy spec should be modified so that calls to internal slots are forwarded just like they are for public slots. I defer to @ljharb on the question of when and how to make that adjustment to the spec; I'm not informed enough on all the relevant considerations to form an opinion on that. And BTW as far as the class fields proposal is concerned, I think this proxy issue is an acceptable temporary downside given @ljharb's comments. It's an edge case that won't even necessarily affect everyone currently relying on proxies, as far as I understand it.

In the meantime, if property-like semantics for private members solves the proxy issue, great, but please let's not unnecessarily group all private properties in a nested object if a simpler model is possible. The more important question is whether a private symbol implementation is a good fit for private members generally, and I think it is.

But I'm still not entirely convinced that implicit brand checks are an absolute requirement. While I think duck typing isn't a strong enough guarantee and agree with brand checking from that perspective, it seems to me that the whole need for implicit brand checks would go away if instance encapsulation were absolute, i.e. if cross-instance private member access were impossible. (Not sure if I'm correct about that though.) WeakMaps and WeakSets might be acceptable workarounds for the use cases that would otherwise require class-level instead of instance-level encapsulation. However, assuming that private symbol / property semantics are a workable solution, this point is irrelevant. And I'm not really opposed to class-level encapsulation anyhow; I was bringing it up mainly in case removing this requirement could somehow help with the proxy issue.

@ljharb
Copy link

ljharb commented Dec 13, 2018

@mbrowne if cross-instance access were impossible, how could i write a static comparison method, that used private data? How could i write any static methods that used private data? Even without cross-instance access, what about .call-ing instance methods?

@mbrowne
Copy link

mbrowne commented Dec 13, 2018

@ljharb

if cross-instance access were impossible, how could i write a static comparison method, that used private data?

The same technique that Babel uses to transpile private fields:

const _ssn = new WeakMap()

class Person {
    name

    constructor(name, ssn) {
        this.name = name
        _ssn.set(this, ssn)
    }

    // writing this as a static method rather than instance method since
    // that's what you asked about
    static checkIfSameSSN(person1, person2) {
        return _ssn.get(person1) === _ssn.get(person2)
    }
}

Sure, it's a lot less ergonomic but it's all about tradeoffs. I'm not convinced myself that it's a good idea to require an inconvenient workaround like this, but in this example and many others, the private member (ssn here) could be a public non-writable property, and the use case of truly needing cross-instance private member access seems pretty rare to me (and maybe even all of them could be addressed by adjustments in the design, but I'd have to think more about it). But neither the proponents of class-fields nor @rdking seem to like the idea of true instance encapsulation, so I doubt it's worth discussing much more unless @rdking thinks it's worth pursuing (which he already said he didn't). Personally I'm on the fence about it and would have to do more research to make up my mind.

Even without cross-instance access, what about .call-ing instance methods?

I'm not sure what you had in mind; here's something totally contrived:

class X {
    let privateProp

    foo(x1, x2 /* x1 and x2 are instances of X */) {
        x1.bar.call(x2)
    }
    
    bar {
        this::privateProp;
    }
}

I can't think of any real use cases, so this seems irrelevant to me (and I don't see why this would require implicit brand checking to be applied generally, only in the case of call/apply), but I'm sure you have some. But let's set aside further discussion of cross-instance access, at least for now—it's probably a moot point.

@rdking
Copy link
Owner Author

rdking commented Dec 13, 2018

@mbrowne

Why couldn't you use a separate private symbol for each property, and keep everything at the top level of the instance?

Private symbol keys can be shared. Whether on purpose or by accident, that makes the property keyed by that symbol public. The approach I've taken hides the key names in the ES engine. That removes the need for name sugar and still successfully avoids death by Proxy without giving up branding. As I understand it, these hurdles aren't things Self had to take into consideration.

As for brand checking: all instances of the same class share the same class signature if that class has privileged members. That signature is the brand.

As for the suit analogy: of course it's sometimes important that the suit is not the wearer. It's the very fact that the suit is not the wearer that affords the wearer the additional capabilities (like surviving in space, being able to see despite unfiltered sunlight, being able to talk in a vacuum, etc...). That's what makes it a good analogy. ;-)

@mbrowne
Copy link

mbrowne commented Dec 14, 2018

I'm not sure which keys you mean when you say, "The approach I've taken hides the key names"--the key for each private property, or the private symbols for the container objects (one per class)? I don't get how you can guarantee that the private symbol and the keys on the object it points to are completely hidden and inaccessible and yet you wouldn't be able to do this in the case of multiple private symbols and without a private member container object. But if that's absolutely the only way you can do it without causing significant problems, I'll take your word for it; this isn't a deal-breaker for me.

But you might consider moving the technical specification details to a separate section at the bottom of the readme and focus the rest of the readme on usage and the developer mental model. I think the average developer is much more concerned about the outward behavior than the implementation details, and I think explaining how there's a private symbol that's internally used to access a container object of private properties is an advanced topic that should be avoided in introductory explanation. And as I said above, I think multiple container objects are the wrong metaphor...at the very least don't use the word "objects" (outside of explanations of the internals)..."property bags" is better at least.

@rdking
Copy link
Owner Author

rdking commented Dec 14, 2018

@mbrowne

I'm not sure which keys you mean when you say, "The approach I've taken hides the key names"--the key for each private property, or the private symbols for the container objects (one per class)?

In any given inheritance chain, each class has it's own private signature and private container, so it's possible that an instance has multiple private containers attached, 1 for each class in the chain with private data. The assurance comes from the facts that:

  • It is impossible to reflect the private symbol names
    • This prevents the keys for the private container from ever ending up being stored in a public property or variable.
  • The only way to access any private container is via operator :: and the class signature of the current execution context.
    • Operator :: prevents the actual private container object from being stored in a public property or variable.
    • The class signature on the current execution context selects the necessary private container property.

Unfortunately, unless private symbols are completely hidden behind syntax in such a way that they cannot be set into a public property or variable, it's possible to unintentionally leak the private key, making the private data public. Likewise, even if syntax is used to hide the individual property keys behind friendly names, monkey patching can still be used to gain access. The only way to guarantee that the private names remain safe from tampering is to prevent them from being exposed.

As for the advice about the developer mental model being in the readme, I didn't realize the readme was used for that purpose! Thanks for the advice.

@mbrowne
Copy link

mbrowne commented Dec 14, 2018

As for the advice about the developer mental model being in the readme, I didn't realize the readme was used for that purpose!

That's how I see it at least. Not just the mental model, but any introductory info for the unacquainted. Notice how the readmes for class-fields, decorators, and classes 1.1 use the readme in this way and provide supplemental documents for more technical (or other) details. But I see no reason why you couldn't have a brief technical summary of the spec changes in the readme if you wanted, as long as it were in its own section and linked to other documents if needed to keep it concise. All of this is just my personal opinion of what makes a good readme for a spec proposal :)

@rdking
Copy link
Owner Author

rdking commented Dec 17, 2018

@mbrowne About cross-instance access:

The point of that feature isn't simply for copy/comparison operations. Consider inheritance. Most uses of this feature come up due to polymorphism. If you have 2 distant cousin instances and one of them requires the other to perform a given internal operation, it can't be done if the cousins can't access each other's internal data. It's a weak example, but try this:

class TextFile {
  let source = null;
  let data = null;
  let convert = (encoding, text) => {
    //returns (text || data) in the appropriate encoding
  }
  constructor({source, text}) {
    if (source) {
      this::source = source;
      data = fs.readFileSync(source);
    }
    else {
      data = convert(`utf-16`, text);
    }
  }
  clear() {
    source = null;
    data = null;
  }
  save(file) {
    if (typeof(file) == "string") {
      source = file;
    }
    if (typeof(file) == "string") {
      fs.writeFileSync(source, data);
    }
  }
  update(overwrite, offset, content) {
    //inserts content into data at offset if !overwrite
    //otherwise overwrites content at offset in data
  }
  //shared === protected. Will exist in a side proposal.
  shared extract(offset, length, encoding) {
    //retrieves a fragment of the text in a given encoding
  }
}

class Document extends Text {
  /* Adds support for arbitrary markup */
}

class PDF extends Document {
  static from(text){
    //Reads data in from other text formats and converts it to PDF
  }
  /* Adds pdf markup to the document */
}

class MarkDown extends Document {
  /* I think you get where I'm going */
}

class SourceCode extends Text {
  /* Adds code completion and syntax highlighting generic support. */
}

class ESCode extends SourceCode {
  /* Adds support for ECMAScript */
}

var src = new ESCode("someFile.js");
var pdf = PDF.from(src);

How do you suppose we do that last line without cross-instance access? Protected implementations are at there most useful when inheritance trees run deep. Similar issues also come up with composition-based designs, especially when it takes more than 1 instance of a given type to perform an operation.

@mbrowne
Copy link

mbrowne commented Dec 17, 2018

If you want data to be accessible from subclasses, couldn't you just make it protected? I wasn't arguing against protected (or shared, as the case may be); I was questioning whether we really need to access private "properties" outside the current instance, i.e. from another instance of the exact same class, not a subclass.

As an aside, I'm not sure that "property" is the best term for the let/const members in this proposal...it implies a bunch of things that are true about regular public properties but are not true of these "properties". Maybe the term "property" still makes sense for protected members, but private members aren't inherited in any way and don't even affect the prototype, right? So they seem very un-property-like. With your latest updates, I suppose they're some kind of hybrid of a variable and a property (at least conceptually), but if I had to pick between those two I'd say they're still closer to "instance variables" from the developer's perspective. Maybe I'm making too big a deal out of this, but I think it would be good to at least formally define "property" as used in this proposal (at some point) if you're sticking with that term.

And while I'm discussing terminology, I still think inheritable would be more precise and significantly less ambiguous than shared (given the desire to avoid protected...I think @ljharb previously made a very good case for why that wasn't a good terminology choice in the first place when other languages used it).

@rdking
Copy link
Owner Author

rdking commented Dec 17, 2018

@mbrowne

If you want data to be accessible from subclasses, couldn't you just make it protected?

Not really protected members are private members with slightly relaxed constraints.

I was questioning whether we really need to access private "properties" outside the current instance

I get it, but it's almost the same question. Protected properties declared by some base present them selves to derived classes as private members. A protected member is conceptually a private member that's been shared with it's descendants. It's still a member of the private container. That's why I used it as an example. If siblings can't access each other, then the code I mocked up won't work either.

I'm not sure that "property" is the best term for the let/const members in this proposal...

I understand your reasoning, but try mine. Private properties need to be understood as being as much properties of the object as their public counterparts, the only difference between them being the need to use a special new operator(::) to access them under certain circumstances. At the same time, declared public properties need to be as lexically available as their private counterparts. Otherwise both the closure metaphor and the property metaphor simultaneously break down.

It's not so much that they are

some kind of hybrid of a variable and a property (at least conceptually)

but rather that they're properties that have been added to the scope chain via "with". Not something easily explained if the properties of the context object don't include the private members. This is also one of the reasons I started using the word "member" to describe all the things accessible from an instance. Properties that aren't publicly accessible don't feel much like the properties we're all used to.

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

No branches or pull requests

7 participants