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

Identifying the goal of the proposal #15

Open
codehag opened this issue Jan 25, 2022 · 17 comments
Open

Identifying the goal of the proposal #15

codehag opened this issue Jan 25, 2022 · 17 comments

Comments

@codehag
Copy link

codehag commented Jan 25, 2022

I see two distinct goals of this proposal:

  1. an ergonomic, intuitive way to check that an object is what it says it is
  2. a way to ensure that a class has fully instantiated.

for 1, I would like to better understand how ergonomic brand checks are doing right now. The happy path for both of these results in the same -- a check if an object is what it says it is. So how is that doing? what are people using it for? Is it primarily library authors? polyfill authors? who did we reach?

As mentioned in your presentation, maybe a "brand check" isn't something developers are actually interested in. Maybe they have a more abstract concept -- something like "instanceof" -- but instanceof is broken in javascript.

As I mentioned, this is a problem that impacts the language more broadly. I'd love to see a broad solution, but maybe we can do something just for classes to solve a subset of that problem.

for 2) we might be able to reuse this for this guarentee, but if this is the primary goal, I would expect a different name. Something like class.initiatedAs(o) or class.isInitiated(o).

@Jack-Works
Copy link
Member

Is it possible to do both

  • constructor completed successfully check
  • prototype check

@XGHeaven
Copy link
Member

I think it's the same goal. It's no sense to check not fully instantiated object.

@codehag
Copy link
Author

codehag commented Jan 26, 2022

I think it's the same goal. It's no sense to check not fully instantiated object.

There is some sense in checking this: object construction can fail. So, if you want to verify an instance is correctly constructed, this proposal provides that mechanism -- after which the author may freeze the object to prevent any modification. It gives you guarantees that the object you are working with has not been tampered with and is complete.

That said -- this doesn't have to be the goal of this proposal. this grew out of the discussion with how this proposal differs from ergonomic brand checks. It certainly has use cases but they are very specific. On the other hand, if we are overlapping in terms of the happy path (constructor does not throw), then the question about "what does an instance mean to developers" is a relevant question to answer. This might be, as @Jack-Works suggested, a mental model which also includes checking the prototype. This would be a good area to explore.

@hax
Copy link
Member

hax commented Jan 26, 2022

if we are overlapping in terms of the happy path (constructor does not throw)

@codehag It's not only "not throw". Actually throw is common. We use throw to express that the request of constructing a new object is not valid.

The happy path we discussed should be:

  • it's not throw
  • if throw, the broken instance should not leak

The self-contradict part of the #x in o proposal is, when we argued whether class.hasInstance or #x in o is better solution for common usage (aka. happy path) in previous meeting, the champions and the supporters of #x in o use the cases of unhappy path to demonstrate #x in o is qualified to add to the language. And after three blocks to that proposal lonely I was so tired and let it go.

I will be VERY VERY VERY frustrate if now committee refuse this proposal by the reason "because the happy path we don't need class.hasInstance".

@codehag
Copy link
Author

codehag commented Jan 26, 2022

I will be VERY VERY VERY frustrate if now committee refuse this proposal by the reason "because the happy path we don't need class.hasInstance".

This isn't what is happening here. I am seeking clarification, which may lead to changing the name in order to better communicate, or possibly adding behavior to address an issue you also brought up -- that "brand check" may not be strictly meaningful for developers.

As I mentioned, for me personally the problem represented here is broader than class instances, and ideally we would look at how this might be done more broadly -- I also know this might be impossible or not your goal. This isn't a blocking concern and we don't need to spend time on it if it isn't of interest.

Edit to add: please note that I explicitly did not block this proposal from advancing as a lone objector. this is all something that can be clarified in a more mature stage, but my preference is that we are on solid ground first.

@hax
Copy link
Member

hax commented Jan 26, 2022

Sorry I may misunderstand the things due to language barrier (this is a long term issue for me and all chinese delegates).

As I mentioned, for me personally the is problem represented here is broader than class instances,

I'm not sure I fully understand your concern. Do u want some more general brand check mechanism not only for classes?

As I said, I don't think brand check is a familiar concept to average programmers. So this proposal is named as "brand check" just to match the TC39 jargon.

And another problem is it's hard for me to imagine how we could have a much general "brand check" out of classes. It seems currently only classes need that. Builtins and platform apis also need brand but it's out of the scope of this proposal, I remember 2021 Jan meeting already have a topic for that. And it's possible that we could have tagged record/tuple or struct which have brands to differentiate the values of different kind of struct, but definitely it could use other syntax, or i guess very likely don't need special syntax.

I also know this might be impossible or not your goal.

I don't treat "impossible", I just not fully understand your (and some others) expectation on this area, so i even don't know whether it could be my goal.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jan 26, 2022

the question about "what does an instance mean to developers" is a relevant question to answer

As a JavaScript developer who wrote and maintains a library (that is about the size of lodash), to me:

  • an instance means an object that obeys the implicit and explicit contract clauses (e.g. preconditions, postconditions, invariants, side-effects, types) indicated by documentation and JSDoc annotations (or similar) for a constructor/prototype (old-style) or class (modern-style). An implicit clause is that an instance must have the expected prototype in its prototype chain.
  • instanceof checks if an object claims to be an instance that obeys the contract.

This is a like the Liskov Substitution Principle, but applied to instances in a dynamically-typed, prototype-based language. However, neither instanceof nor brand check could, in general, guarantee that an object passing the checks does actually obey the contract. No guarantee is needed for instances/prototypes that don't escape a suitable boundary.

Based on my current understanding, the utility of brand check, for me, would be that the runtime could provide the desired guarantee for classes I author, or review, when the prototype and all constructed instances are frozen.


Edit: the class constructor might also need to prevent extension by unknown sub-classes. I haven't tested, but I think this could be done by checking new.target if needed.

@codehag
Copy link
Author

codehag commented Jan 26, 2022

And another problem is it's hard for me to imagine how we could have a much general "brand check" out of classes. It seems currently only classes need that. Builtins and platform apis also need brand but it's out of the scope of this proposal, I remember 2021 Jan meeting already have a topic for that.

yes, this is a harder problem. I don't know how to solve it myself. But this is what I mean by more general. This is also one of the more difficult parts of javascript and the spec just because of how dynamic things are.

I think this proposal can be justified in it's own right, but there are two independent reasons for this proposal to exist: 1) a descriptive api (better for reading and understanding), and 2) a way to guarantee the initialization of the class. These two distinct things are linked by the history of the proposal rather than by the intent.

In my initial comments, I was focused on the first -- if we are adding this new ergonomics, how has the existing proposal been doing, what can we learn from that? We don't need to rush forward with proposals, we can take our time and fully understand the holes left behind by previous work. I won't press on this issue though.

For the second one, I don't think many developers would realize that class.hasInstance communicates this just from the name. However this second behavior in particular adds quite a bit of value for contexts which require high integrity (mark's comment), and is what brought this to stage 1.

Correct me if I am wrong, I think you may be more interested in the ergonomic use case? So my question in this case is, is this a complete solution for this use case? Including situations like what shu described about changing the prototype chain? Traditionally, instanceof in JS has been checking for the constructor on the prototype chain. Thinking out loud here...:

class Z { z = 1; checkMe(o) { return class.hasInstance(o) } }
class Y extends Z { y = 2; checkMe(o) { return class.hasInstance(o) } }
class X { x = 3; checkMe(o) { return class.hasInstance(o) } }

const zInst = new Z();
const xInst = new X();
const yInst = new Y();

yInst instanceof Z // true
zInst.checkMe(yInst) // true
yInst instanceof Y // true
yInst.checkMe(yInst) // true
yInst instanceof X // false 
xInst.checkMe(yInst) // false

Object.setPrototypeOf(yInst, xInst);

yInst instanceof Z // false
zInst.checkMe(yInst) // true
yInst instanceof Y // false
yInst.checkMe(yInst) // false
yInst instanceof X // true 
xInst.checkMe(yInst) // false

Sorry for the million edits. I am likely misunderstanding something, as I quickly sketched this out with ergonomic brand checks. I thought i got it wrong, then it turns out i got it wrong in the wrong way. Here is the full polyfill version:

var _set1 = new WeakSet();
var _set2 = new WeakSet();
var _set3 = new WeakSet();

class X {
  constructor() {
    _set1.add(this);
  }

  equals(range) {
    return _set1.has(range);
  }
}
class Z {
  constructor() {
    _set3.add(this);
  }

  equals(range) {
    return _set3.has(range);
  }
}
class Y extends Z {
  constructor() {
    super();
    _set2.add(this);
  }

  equals(range) {
    return _set2.has(range);
  }
}


var yInst = new Y;
var xInst = new X;
var zInst = new Z;

console.log(yInst instanceof Z) // true
console.log(zInst.equals(yInst)) // true
console.log(yInst instanceof Y) // true
console.log(yInst.equals(yInst)) // true
console.log(yInst instanceof X) // false
console.log(xInst.equals(yInst)) // false

Object.setPrototypeOf(yInst, xInst);

console.log(yInst instanceof Z) // false
console.log(zInst.equals(yInst)) // true
console.log(yInst instanceof Y) // false
console.log(yInst.equals(yInst)) // false.
console.log(yInst instanceof X) // true
console.log(xInst.equals(yInst)) // false

Ok, what would be the ideal situation here? I was thinking that one would be related to the prototype, and one related to how the object was initially constructed.

Again, sorry for any confusion from me editing the post 100 times.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jan 26, 2022

If class.hasInstance would be understood to be a fixed instanceof, I think many developers would intuitively expect

  • when instanceof is false, then class.hasInstance must be false
  • when instanceof is true, then class.hasInstance can be false or true
  • when class.hasInstance is false, then instanceof can be false or true
  • when class.hasInstance is true, then instanceof must be true

Otherwise the following might become a generally recommended 'best practice'

class C { static isC(o) { return o instanceof C && class.hasInstance(o) } }

So, I think Jack Works' suggestion would need to be assumed.

@hax
Copy link
Member

hax commented Jan 26, 2022

  • an ergonomic, intuitive way to check that an object is what it says it is (much like the goal of a brand check)
  • a way to ensure that a class has fully instantiated.

@codehag

I don't think these two things is separated, at least for most people who use classes, follow the OOP concept, "fully instantiated" is the prerequisite of "an object is what it says it is".

There may be the gap: I don't think people "check that an object is what it says it is" means they want to do "brand check". This proposal add class "brand check" as the mechanism of "check instance", but "brand check" is not the goal. We also don't want to expose "brand" concept to developers.

What "brand" check is ?

As I understand, a "brand" as TC39 term, means some code have the power to classify the objects into two category: those have the brand, those not have. And no other code can change or forge it.

So it's a very general concept, most time too general.

We already have the right tool for branding, that's weakset. I don't think we need any other feature to make branding much ergonomic in general way. Because 1. it seems just invent another way to do the same thing, 2. you can't make a "too general" thing really ergonomic, because there is no enough context.

This is why I feel #x in o is wrong --- Private fields are for internal states, private methods/ are for internal operations. They are not designed for branding. The use cases of per field checking are just branding usage. They just do it in a different way for taste or engineering reason (gc or perf effect in engines). And, if u see their pattern, using inheritance and base class return trick , the pattern is already geek enough, it make no sense to improve the ergonomic for geek usage, and also #x in o doesn't really improve the ergonomic much in such pattern.

On the other side, the common usage of the private fields do not need per field check.
Logically, an object has the private field because "it's the object what it says it is", not: "it's the object what it says it is" because the object has the private field. Using "#priv in o" to "check that an object is what it says" inverse the effect and the cause, so it's no surprise that it has the flaw that hard to use "#priv in o" to ensure "fully instantiated".

So "check that an object is what it says" is really the task we should provide much ergonomic, intuitive way for programmers. And "check the object has fully instantiated by the class" is the part of the definition of "check that an object is what it says". It's doesn't make sense to say it's the object what it says but not fully instantiated or in a broken state. So I think that's why @XGHeaven said "it's the same goal."

@codehag
Copy link
Author

codehag commented Jan 26, 2022

I think we are talking past each other. Lets take ergonomic brand checks out of the picture. It has been an unhelpful distraction to this proposal.

You have mentioned that you do not actually care about the completed object -- thats also fine, and it answers my question. So we can ignore item 2.

The problem, as I understand, that you want to solve is that instanceof doesn't really work. You mentioned (and I fully agree) that "brand check" doesnt mean much to developers. The question raised at plenary by shu was "what guarantees should this provide". Let's check our shared understanding of that, using the following code:

var _set1 = new WeakSet();
var _set2 = new WeakSet();
var _set3 = new WeakSet();

class X {
  constructor() {
    _set1.add(this);
  }

  equals(range) {
    return _set1.has(range);
  }
}
class Z {
  constructor() {
    _set3.add(this);
  }

  equals(range) {
    return _set3.has(range);
  }
}
class Y extends Z {
  constructor() {
    super();
    _set2.add(this);
  }

  equals(range) {
    return _set2.has(range);
  }
}


var yInst = new Y;
var xInst = new X;
var zInst = new Z;

Object.setPrototypeOf(yInst, xInst);

console.log(zInst.equals(yInst)) // what should this be and why?

@ljharb
Copy link
Member

ljharb commented Jan 26, 2022

The way I’d describe it is: zInst has all the slots/fields/constructor behavior of Z and Y, and has all the methods of Z, Y, and X - so it’s a “true instance” of Z and Y (because it meets both sets of criteria) and it inherits from X (which may, or may not, allow it to “work like an X”, depending on X’s internal implementation details).

Knowing the former means i can borrow methods (.call, etc) and expect them to work reliably; knowing both means i can directly call methods reliably (assuming method also haven’t been replaced, which is a separate issue); knowing only the latter requires me to make assumptions or know implementation details.

@codehag
Copy link
Author

codehag commented Jan 26, 2022

The way I’d describe it is: zInst has all the slots/fields/constructor behavior of Z and Y, and has all the methods of Z, Y, and X - so it’s a “true instance” of Z and Y (because it meets both sets of criteria) and it inherits from X (which may, or may not, allow it to “work like an X”, depending on X’s internal implementation details).

To quickly clarify, In this case (sorry it isn't well written), Y inherits from Z, not Z inheriting from Y. So Z does not have access to Y's slots. The prototype has been moved for yInst from Y & Z, to xInst (X). You can see a full runnable example of this here

@ljharb
Copy link
Member

ljharb commented Jan 26, 2022

Right; but it means Y’s methods can access Y’s fields, and Z’s methods Z’s; I’m assuming each class’ methods are written to use their own fields.

@codehag
Copy link
Author

codehag commented Jan 26, 2022

Interestingly we have the following for yInst in this case: console.log(yInst.equals(yInst)) // false (this makes sense, i just wasn't expecting it with the model i was using here)

but it means Y’s methods can access Y’s fields, and Z’s methods Z’s

Right, so -- this was what i was thinking of when i thought "it is interesting to be able to verify what this was constructed with, not what it might be now. I wonder if this translates to an OOP model as hax is citing, in a language that is prototypical, to something that should be called an "instance" -- especially since it is giving a fundamentally different answer than instanceof. For this case, which i think is valid, i would expect this to be named something like class.isConstructorOf(o) or class.isInitializedAs(o), to make this intended behavior clear. This was what I was trying to get across in my first comment as "goal 2".

... isConstructorOf also references prototype.constructor, but in some ways that is the underlying ambiguity clarified by this. This would be a complementary relationship.

@ljharb
Copy link
Member

ljharb commented Jan 26, 2022

class.constructed(o)

@codehag
Copy link
Author

codehag commented Jan 26, 2022

yep, i think that would also work. I do think that this is a valuable piece of information to have and would warrant this proposal. This issue would be resolved as a naming bikeshed in that case.

But i also respect what hax has been saying here, and the group that he has been working with is not interested in this property by itself -- rather they are interested in a concept of an OOP "instance", which is different how javascript has been treating it in the prototypical paradigm. This is not well represented by this constructed check alone. For example, right now console.log(zInst.equals(yInst)) will return true, however yInst will throw if it calls any methods defined on Z. i agree with the proposal jack made to address this as a potential solution, though we should discuss expectations first. Let's see what hax says when he has a chance to review this.

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

6 participants