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

Clarification on proposed meta information #12

Closed
uhop opened this issue Aug 2, 2017 · 7 comments
Closed

Clarification on proposed meta information #12

uhop opened this issue Aug 2, 2017 · 7 comments

Comments

@uhop
Copy link

uhop commented Aug 2, 2017

TAXONOMY and METAPROGRAMMING define a structure to describe a decorated element:

{
  kind: "method" or "field"
  key: String, Symbol or Private Name,
  placement: "static", "prototype" or "own",
  descriptor: Property Descriptor (argument to Object.defineProperty),
}

It appears that kind is largely redundant: it can be derived from a property descriptor:

function getKind (prop) {
  return prop.get || prop.set || typeof prop.value == 'function' ? 'method' : 'field';
}

Granted that someone can define a field initialized with a function, but it is … a method for all intents and purposes, and probably should be treated as such.

"Class decorators are passed in an Array of all class elements". Why exactly? To check elements by names linearly? To have them unsorted by placement? It appears that if we re-use an existing structure we gain utility and simplicity. I am talking about a dictionary of property descriptors consumed directly by Object.defineProperties() and Object.create(). If a class decorator receives three objects corresponding their placement, and can modify them in place, it will simplify a lot of things. (Obviously a class decorator needs to know the base class as well.)

Example: verify that render() is defined, add a default implementation otherwise:

const ensureRender = (proto, stat, own) => {
  if (!proto.render && !own.render) {
    proto.render = {value: (() => {}), configurable: true, writable: true};
  }
};

The example above is trivial. The array version will be less readable and much slower.

Example: do-it-yourself class in a class decorator:

const doItYourself = (proto, stat, own, Base) => {
  // merge our custom code with initialization of own
  const Ctor = makeConstructor(proto.constructor, own, Base);
  // take care of the prototype
  Ctor.prototype = Object.create(Base.prototype, proto);
  // take care of static definitions
  Object.defineProperties(Ctor, stat);
  // ...
};

Again, all code above is a small collection one-liners. The only non-trivial part is to generate a constructor, which will call the base, initialize own properties, and call a user-defined custom constructing code. It should be done anyway is a part of this proposal.

The proposal mentions, but does not define, a class finisher. The code above could very well be a spec for the default finisher.

Example: go back to the original array of elements:

const getKind = prop => prop.get || prop.set ||
  typeof prop.value == 'function' ? 'method' : 'field';

const getKey = name => 
  typeof name == 'string' && name.charAt(0) === '#' ?
    decorators.PrivateName(name.substr(1)) : name;

const generateElement = (placement, props) => name => {
  const prop = props[name];
  return {placement, key: getKey(name), kind: getKind(prop), descriptor: prop};
};

// convert to the old structure in a class decorator
const converter = (proto, stat, own) => {
  const old = [
    ...Object.getOwnPropertyNames(proto).map(generateElement('prototype', proto)),
    ...Object.getOwnPropertySymbols(proto).map(generateElement('prototype', proto)),
    ...Object.getOwnPropertyNames(stat).map(generateElement('static', stat)),
    ...Object.getOwnPropertySymbols(stat).map(generateElement('static', stat)),
    ...Object.getOwnPropertyNames(own).map(generateElement('own', own)),
    ...Object.getOwnPropertySymbols(own).map(generateElement('own', own))
  ];
};

Isn't it simpler and more flexible this way than an array of elements?

@ljharb
Copy link
Member

ljharb commented Aug 2, 2017

Differentiating between a function and a method is important because of [[HomeObject]]/super, and because you can't construct methods.

@uhop
Copy link
Author

uhop commented Aug 2, 2017

I think that super is an internal business of a function or a method. A method is not necessarily uses it.

Could you give realistic examples/use cases, where this distinction is important?

@ljharb
Copy link
Member

ljharb commented Aug 2, 2017

The decorator might want to install a function that uses super.something, and it can't do that for a function-valued property, only for a method.

@uhop
Copy link
Author

uhop commented Aug 2, 2017

Unfortunately it may not. At least not now:

$ node
> x = function () { super.x(123); };
x = function () { super.x(123); };
                  ^^^^^

SyntaxError: 'super' keyword unexpected here

>

Any attempt to create a function with super outside of a class method definition doesn't work.

The same goes for copying member functions between prototypes:

$ node
> class A { x() { console.log('*** class A method x()'); } }
[Function: A]
> class B extends A { x() { super.x(); } }
[Function: B]
> new B().x();
*** class A method x()
undefined
> class C {}
[Function: C]
> C.prototype.x = B.prototype.x
[Function: x]
> new C().x();
*** class A method x()
undefined
>

The example above demonstrates that a static (one-time) binding is used as per the spec, and copying does not change that.

If the proposal includes things, which are impossible at the moment, please at least mention them in the documentation — I've read all of it thoroughly and do not recall seeing anything like that.

Unless it is proposed to change, my reasoning stays, and the question "why so complex?" remains.

@littledan
Copy link
Member

There are a couple reasons why I separated out method/field kinds:

  • Methods are all installed before the first field is installed.
  • The initial value of a method is a constant, whereas the initial value of a field is found by running the thunk.

I'm not sure how to unify them given these differences. A kind field is also a sort of future-proofing in case more differences come up in the future.

The way that methods have special rights for super isn't part of this proposal, it's part of ES6. At that time, it was proposed to expose a MakeMethod function which would give a function a HomeObject and the ability to call super. Reviving such a proposal would be a useful complement to this proposal.

@littledan
Copy link
Member

It appears that if we re-use an existing structure we gain utility and simplicity. I am talking about a dictionary of property descriptors

I don't see a way to make a dictionary that includes private class elements.

@littledan
Copy link
Member

I don't really see any actions to take for this bug; the suggestions seem to really be around the ergonomics of the decorator functions themselves, which I don't think have to be the prettiest, and I've explained the design rationale above.

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

3 participants