erights / Orthogonal-Classes

Proposed EcmaScript Class Syntax clarifying orthogonal concerns
29 stars 2 forks source link

Constructor semantics questions #7

Open littledan opened 7 years ago

littledan commented 7 years ago

In this design, if the "constructor" method definition is made static, then it is initialized to point at itself.

What does this wording mean, that the constructor has a property named constructor to point to itself, and the prototype doesn't get such a property?

I believe you can already have a static method named constructor, and it just works like an ordinary static method--the body is not used as the constructor, and it doesn't change what happens when you [[Construct]] the class. Maybe it's web-compatible to change (certainly it's doable for browsers to count how many websites make static methods named constructor) but the change should be noted.

If made private, no matter where it is placed, only code inside the class can follow this pointer back to the class/constructor. Clients of the instances have no such access and so cannot so navigate.

I'm not sure I understand. Does this mean that the constructor property would not be added to the prototype, and instead there's a #constructor field to do the same, but otherwise the class behaves the same way? (If so, seems fine to me.)

Is it OK that the private constructor pattern for blocking doesn't give you a way to access the implicit default constructor definition? I could imagine a decorator for removing C.prototype.constructor which wouldn't prevent the default constructor from being used. (I don't see how that decorator would introduce a #constructor field, but I'm not sure it's necessary, given that you can just access the class with a lexically scoped variable from within the class body.)


Should we also have own constructor too, making an own property named constructor in place of the prototype's property?

For all of these, I'm wondering why declaring a constructor in one of these other ways would do anything to get rid of the "prototype constructor". For ordinary method/property/field declarations, things coexist just fine. Is this all about the way that an implicit constructor is generated, that anything with the same name should squelch that process and subsume it? My intuition would've been that they're in different "namespaces" and wouldn't contravene each other that way.

allenwb commented 7 years ago

also see https://github.com/erights/Orthogonal-Classes/issues/3

I also think there are several issues with the concept of constructor placement/visibility as described by @erights in the proposal.

I made the following table to clarify what @erights described, but didn't put it in because I realized it didn't make much sense.

constructor visibility member defined
constructor() {} Prototype constructor property initialized to constructor function.
#constructor() {} No constructor property on prototype. Prototype #constructor private field initialized to constructor function.
static constructor() {} No constructor property on prototype. constructor property of constructor function initialized to constructor function.
static #constructor() {} No constructor property on prototype. Prototype #constructor private field of constructor function initialized to constructor function.
own constructor() {} No constructor property on prototype. constructor property of each instance initialized to constructor function.
own #constructor() {} No constructor property on prototype. Prototype #constructor private field of each instance initialized to constructor function.

The case that seems like it might be most useful is #constructor(){} as it seemingly makes the constructor privately available to to the class body without publicly exposing it as an instance accessible property. But that essentially makes the constructor a "private methods of the prototype" and the proposal already has disallowed prototype private methods methods because there isn't a good way to access them from within the class body (in particular from within other prototype methods). #constructor on the prototype would have all of those same access issues.

static constructor(){} (as defined in the table) and static #constructor() {} are accessible if the class is named but really just provide a longer way to express what can already be expressed:

class Foo {
   bar(){return new Foo.#constructor }  //means same as `return new Foo`
}

own constructor(){} and own #constructor(){} don't have have the accessibility issues of a private constructor field on the prototype. But adding an extra slot to every instance seems unnecessarily expensive was a way to limit public access to a constructor via instances.

Rather than applying this full but limited utility generality to constructor I image two alternative special case solutions for restricting access to the constructor.

1) special treatment of #constructor Generally disallow placement prefixing of constructor and #constructor (except for static constructor(){} which is currently allowed by ES2015 with no special meaning). However, allow #constructor() {} with special case semantics that means don't add a constructor property to the prototype (and also don't define a #constructor field on the prototype).

2) lexical declaration of constructor function Assuming we allow function declarations to lexically occur within a class body we could provide special case treatment for functions named constructor. In particular, we could recognize syntax like this:

class {
   function constructor() {} //define the class' function body
   bar() {new constructor()}; //lexical reference to current class constructor
}

Basically, a class element that is a function declaration for constructor would provide the function body of the class' constructor and it would lexically bind that function to the name constructor within the body of the function. It would suppress creation of a prototype property named constructor.

Of these two alternatives, I currently prefer the second.

erights commented 7 years ago

It turns out I was wrong about the utility of "private constructors" expressed by any of these means anyway. It does not protect against the hazard under naive subclassing. (attn @dtribble , @fudco )

Given

class Foo {
    static #constructor() {} // or any of the other proposed privacy forms
}

the Foo constructor is indeed not accessible from direct instances of Foo. However, if there's a later naive

class Bar extends Foo {
    constructor() { super(); }
}

then the Bar constructor us accessible from instances of Bar. The Bar constructor visibly inherits from the Foo constructor. And instances of Bar are still (indirect) instances of Foo.

This does not argue against making the current proposal as orthogonal as possible. But it does remove this utility argument as an additional argument for this design.

allenwb commented 7 years ago

I'm not sure what hazard you are concerned about. I thought the main concern was that a reference to an instance of Foo could be restricted such that it did not also grant access to Foo itself. Regardless of the syntax, all of the "private constructor" schemes that block creation of a prototype constructor property have that characteristic.

For the subclassing case you show here, the code in question already has access to Foo and hence the ability to invoke it as a constructor. This comes with access to Foo and has nothing to do with subclassing. There are many ways that any code that has access to Foo could leak it. Why is subclassing any more of a concern than any of the other ways of leaking it?

erights commented 7 years ago

That code has access to Foo. That is not the problem. The problem is that instances of Bar grant access to Foo. If the author of Bar intends this, no problem. Bar's author provides only access they have a right to provide. However, it might not have been Bar author's intention to override this aspect of Foo's design. The problem is that this override happens anyway, when Bar's author is just following the normal way of doing things.

littledan commented 7 years ago

Given that the #constructor pattern doesn't get inherited as we'd hope for such a mechanism to do, would it be reasonable to leave the constructor things out of this proposal for now? Maybe we can just ban private methods and fields named #constructor to future-proof for it in a later proposal.

erights commented 7 years ago

@littledan Yes, I think that is reasonable. I like the future proofing point. It means we can take this suggestion by "simply" adding these other constructor cases to our exclusion matrix for now.

Leaving this open for the moment anyway to gather more comments. But yet, I expect we will take your suggestion.