-
Notifications
You must be signed in to change notification settings - Fork 37
Should private methods and accessors type-check their receiver? #1
Comments
I think many of the arguments for type-checking when accessing fields apply equally well when to type-checking methods, except possibly the argument from implementability, so I'm lightly in favor of it unless there's good reason not to. I think that most of the reason you might want not to - in particular, for separating and sharing implementation logic which doesn't necessarily operate on instances - would be adequately handled by using static private methods operating on their arguments. |
First terminological nitpick: By "immutable" I think you mean what I would call "unassignable", or in JS a "const" binding. Specifically, in your example you cannot reassign #sayHi to be bound to something else, whether on a per instance basis or not. Regarding object state, "immutable" falls in the middle of a hierarchy of restrictions on mutability:
https://people.eecs.berkeley.edu/~daw/papers/pure-ccs08.pdf |
Second terminological nitpick: "Type" has many meanings, especially in JS currently. I think it is better to talk about the class branding its instances and the question of whether private method invocation should first dynamically verify the brand. We could say that a class represents a nominal type and that the brand check is a dynamic nominal type check. This is accurate but requires some explanation. JS classes are generative, so nominal type identity is also generative. I do not know if this falls within most people's understanding of nominal types. The following seminal papers refer to this as trademarking rather than branding, which I am open to. I have used both terms, and I have occasionally used branding for something different. |
Whew! With all that said, I favor private methods being unassignable and brand-verifying on invocation -- before running any user code within the function parameter list or body. Likewise static private methods should do an identity check on their receiver, the constructor object. No separate brand is needed since only this one object would ever carry the brand, and there is no independent mechanism for observing whether something is branded. I like how all this can still be rationalized as being within orthogonality because
|
Btw, I wonder about including private accessors. It might be the right thing. But it is weird that there would be no way to retrieve the accessor's getter or setter function. It would be like, in your example, allowing |
I've previously expressed my opinion at tc39/proposal-class-fields#1 (comment) I've also discussed elsewhere that, given our current concept of lexically scoped private names, the most straight-forward semantics for "private methods" is essentially that of lexically scoped function declarations with a special invocation form. Brand checking "private methods" does not add any safety to the language but it does add runtime overhead. It also requires adding some sort of class branding mechanism (something different from private fields) to the low level ES object model. That is a heavy-weight addition that seems to add no value. ES is a dynamically typed language. Private methods should not be a back door for introducing dynamic nominal typing. Finally, here is another way that brand checking private methods would limit their utility. Imagine a class: class Example {
complexMethod1() {
//many
//lines
//of code
}
complexMethod2() {
//many
//lines
//of code
}
} Note that this method is fully subclassable and that complexMethod1 and compelxMethod2 fully work on subclass instances. Overtime, a developer may realize that there is significant overlap in the code of the two methods and that some shared procedural decomposition would make the code more maintainable. It is desire to do this in a manner that does not expose the subprocedures to direct pubic use. So it might be rewritten as: class Example {
#sub1() {}
#sub2() {}
#sub3() {}
//etc.
complexMethod1() {
//some code
this.sub1();
this.sub(3)
}
complexMethod2() {
//some other code
this.sub2();
this.sub3();
}
} With per-class private method branding, this would break for subclasses (or require a much more complex, subclass aware branding scheme). |
What they really do is a check for the existence of the referenced field. I wouldn't call that a brand check as it doesn't necessarily imply anything about the accessed object over and beyond the existence of that specific field. This would even be more the case if we went in the direction described in tc39/proposal-private-fields#93 (comment) |
@allenwb, on your last point about procedural decomposition, that's what I meant to address in my second paragraph: i.e., it seems to me that static private methods operating on their arguments adequately handle the procedural decomposition use case, when operating on objects which are not necessarily instances of the class. But perhaps instance private methods would be easier to reason about. |
I would expect a private method to be accessible on a given receiver iff a similarly declared private field were accessible. So I don't think this would actually require adding much to the language. |
Sorry, to clarify about how branding would work: I'd expect that each class would give a brand in an additive way: when returning from super(), the reciever has a brand added to it which permits all of the private methods to be called. That implies that, if you have subclassing, then methods defined in superclasses are able to call private methods defined in those superclasses. |
So a brand check would have to check against a set of available brands?? Doesn't that make the check even more expensive. What is the actual benefit that justifies that cost? |
The check for private fields is conceptually similar to an own properties lookup except the key is an internal private field identifier rather than a string or symbol. It sounds like you are saying add the equivalent of a private field corresponding to each private method to each instance objects. |
Yes. I don't think the following (cool) paper is relevant, but in case it is: For private field Allen, please remind me. In your proposal, for private method |
I what I have suggested (I need to find where it was written down???) class E {
#sayHi() {}
} is pretty much the same as class E {
function sayHi() {}
} except that #sayHi is lexically bound as a private name rather than a normal identifier. Also private name bindings need to distinguish whether a private name is bound to a field or to a function. The other characteristic is that the syntactic form So, if |
If In any case, I object to the non-uniformity. It breaks the illusion that |
Yes. In fact, no matter what the actual semantics are, I'm reasonably confident that the common mental model will be roughly function f(){}
class A {
#m = f;
} which would imply a brand check before invocation. Apart from making |
Right, actually I originally wrote |
OK, it seems like at this point, we're on the same page about the two options. Sorry for my sloppy wording, and thanks for clearing it up everybody. @allenwb, aside from implementation cost/overhead, what downsides do you see from the brand checking approach? We'll get the early error on a missing method either way. Another point: If we don't do checking here, should we also not do receiver checking for static private fields, and just make those lexically scoped declarations as well? Maybe this is an argument in favor of doing the brand checks. As for user intuition: I think we're talking about an edge case here that won't factor much into user intuition one way or another. I expect normal user intuition to be more like, "It's just like a normal method in a class, except I can't use it outside of the class at all." However, it is important to make a good call here, as I bet code will be written to take advantage of no checking if that's the side we end up coming down on, and we'd have to be comfortable with that. E.g., if we don't do checking, there's no difference between a static and non-static methods. |
Yes, it is a total illusion. Which is why I've argued that lexically scoped function declarations is enough to support all of the reasonable "private method use cases". The major push back was the desire to use dot notation for invocation rather than adding an explicit Treating foo.#sayHi() as a special form addresses that invocation concern. But .#id() has to be thought of as complete special form, not as a .# followed by a function invocation. Personally, I favor that irregularity in the meaning of . over the complexity that any sort of checked lookup would require. Yes, in this case, mere verification is an adequate form of lookup. But if the value of foo is something irrelevant, nowhere else in the language will dot succeed at looking up what is sought. |
It occurs to me belatedly that there are in fact two times you could reasonably check the type of the receiver - on access, and on invocation. These can be different, in principle - class A {
#p() {}
m(other) {
const f = this.#p;
f.call(other);
}
} I certainly wouldn't want to require engines to do the check twice, though I imagine it could be elided in the common case. |
I wrote some spec text which attempts to implement the type checking decision from here. You can see it in https://littledan.github.io/proposal-private-methods/ . Any thoughts? |
This is the main open question for observable private method semantics: Should private methods act like a lexically scoped function declaration, or like (immutable) fields on the instance/constructor? Or, to give an example, what should the following code do:
If we treat
#sayHi
basically like a lexically scoped function, that happens to be in method position, then thealert('hi')
should run just fine without any issue. On the other hand, if we treat it as an immutable field, then it would throw aTypeError
because the#sayHi
field doesn't exist onundefined
.The current spec text does not do the type checking, treating it as a lexically scoped function. If we want to add type checking, I think we'd want to take the following into account:
The text was updated successfully, but these errors were encountered: