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

'kind' value is 'method' on decorator descriptor with get/set property definition #162

Closed
pabloalmunia opened this issue Oct 29, 2018 · 36 comments

Comments

@pabloalmunia
Copy link
Contributor

I'm not sure, but I understand that when we decorate a property with get and set methods the descriptor that is received in the decorator should be of kind 'field' and the get and set method are included into de property descriptor. Now the kind value is 'method' as is inferred from:

If element.[[Kind]] is "field", then
element.[[Initializer]] is present.
element.[[Descriptor]]'s [[Get]], [[Set]], and [[Value]] slots are absent.

This a very simple example with Babel 7.1:

function check(...args) {
  console.log(...args);
}

class Test {
  
  @check
  y() {
    console.log('y');
  }
  
  @check
  z = 10;
  
  @check
  set x(v) {
    console.log('set x');
  }
  get x() {
    console.log('get x');
  }
  
}

The result is:

Object [Descriptor] {
  kind: 'method',
  key: 'y',
  placement: 'prototype',
  descriptor:
   { value: [Function: value],
     writable: true,
     configurable: true,
     enumerable: false } }
Object [Descriptor] {
  kind: 'field',
  key: 'z',
  placement: 'own',
  descriptor: { configurable: true, writable: true, enumerable: true },
  initializer: [Function: value] }
Object [Descriptor] {
  kind: 'method',
  key: 'x',
  placement: 'prototype',
  descriptor:
   { set: [Function: value],
     configurable: true,
     enumerable: false,
     get: [Function: value] } }

In my humble opinion the kind property of the last case must be 'field'.

I apologize in advance if I am wrong.

@ljharb
Copy link
Member

ljharb commented Oct 29, 2018

Fields are own properties on the instance, that's a getter/setter pair on the prototype - i agree that calling it a "method" is a bit confusing, but it's definitely not a field.

I wonder if we could change the "kind" to be something like "accessor"?

@pabloalmunia
Copy link
Contributor Author

placement: 'own' and placement: 'prototype' are the right way for identify when the property is defined into the prototype or into the object. It's not necessary define other kind.

@ljharb
Copy link
Member

ljharb commented Oct 30, 2018

That's true, but there is a difference between a field (which has a per-instance initializer), a method (a function), a data property, and an accessor property, which doesn't seem currently captured by the "kind".

@pabloalmunia
Copy link
Contributor Author

You can use the descriptor for check if a get/set or a value is defined for this property, but is a indirect route. Off course, I can check the kind is 'method' and descriptor has a get or set function. Perhaps the 'accessor' kind is a good solution.

@littledan
Copy link
Member

FWIW a much earlier draft from @wycats used "accessor" here. I'd be OK either way--I chose "method" since it seemed unnecessary to represent the information in multiple places: the property descriptor already lets you know it's an accessor.

@pabloalmunia
Copy link
Contributor Author

pabloalmunia commented Oct 30, 2018

It's always possible to identify the kind with the descriptor. For example, a method can be checked with typeof descriptor.value === 'function', but it is much easier to do it with kind === 'method'.

Similarly, we can identify a get / set with checking typeof descriptor.get === 'function', but it would be much simpler with kind === 'accessor'.

kind is a very short way for this type of identification, but is limited with properties defined with accessor functions.

@littledan
Copy link
Member

@pabloalmunia Can you tell me about a case where you want to make this distinction from inside a decorator?

@pabloalmunia
Copy link
Contributor Author

@littledan It is a little complicated to include a real example here. We are building an extension framework based on decorators with more than 10,000 lines of code.

We have prepared this simple case step by step.

Step 1.- Simple case:

function intercept(desc) {
  if (desc.kind === 'method') {
    const originFn = desc.descriptor.value;
    desc.descriptor.value = (...args) => {
      console.log(`call ${desc.key} with this parameters`, ...args);
      return originFn(...args);
    };
  } else {
    // field
  }
  return desc;
}

class Demo {
  @intercept
  x (a, b) {
    return a + b;
  }
  
  @intercept
  z = 10;
}

const d = new Demo();
console.log(d.x(10, 20));

Work without problem and intercept the method call and ignore the property.

Step 2.- Add get / set without change into the decorator:

class Demo {
  @intercept
  x (a, b) {
    return a + b;
  }
  
  @intercept
  get w() {
    // ...
  }
  set w(v) {
    // ...
  }
  
  @intercept
  z = 10;
}

The code is broken because the descriptor is wrong when the case is a get/set accessor:

TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>

Step 3.- Add support for get / set accessor:

function intercept(desc) {
  if (desc.kind === 'method') {
    if (desc.descriptor.get) {
      const originalGet = desc.descriptor.get;
      desc.descriptor.get = () => {
        console.log(`get ${desc.key}`);
        return originalGet();
      };
      if (desc.descriptor.set) {
        const originalSet     = desc.descriptor.set;
        desc.descriptor.set = (v) => {
          console.log (`set ${desc.key} with value`, v);
          return originalSet ();
        };
      }
    } else {
      const originFn = desc.descriptor.value;
      desc.descriptor.value = (...args) => {
        console.log(`call ${desc.key} with this parameters`, ...args);
        return originFn(...args);
      };
    }
  } else {
    // field
  }
  return desc;
}

class Demo {
  @intercept
  x (a, b) {
    return a + b;
  }

  @intercept
  get w() {
    // ...
  }
  set w(v) {
    // ...
  }

  // @intercept
  // z = 10;
}

const d = new Demo();
console.log(d.x(10, 20));
d.w = 20;
const n = d.w;

In our real code the problem is more complicated, and perhaps this example is an extreme simplicity, but we need to differentiate the methods of the access descriptor with get / set in many situations.

Kind regards.

@littledan
Copy link
Member

I would be fine with this change, but I'm still having trouble understanding the complexity. The logic in the specification would be the equivalent of the JS code, desc.kind = 'value' in desc.descriptor ? "method" : "accessor". Would such a snippet work for you?

@pabloalmunia
Copy link
Contributor Author

Yes, it is an elegant solution. As a result, desc.kind can have three values: method, field or accessor and it is very easy to differentiate between them. I will talk to my coworkers to present to you a clearer example of this complexity.

We appreciate your attention and we are very grateful for the new decorator specification.

@littledan
Copy link
Member

Thanks! I'd love to hear more about your project.

@mbrowne
Copy link
Contributor

mbrowne commented Oct 30, 2018

I assume that in the case of getters and setters, in most cases you'd only need to decorate whichever appears first?

Also, I'm wondering if three categories for the kind of class elements are really necessary. Maybe the real issue is that the term field is misleading in the case of accessors. Another option could be dataProperty, which would mean either a field or an accessor pair. The only thing I don't like about that name is that you can set a field to any object, so some might say it's not really a "data property" in that case. The naming is hard, but from the technical side I don't see where you'd need a third category in order to actually implement a decorator (which seems to also be @littledan's logic).

@nicolo-ribaudo
Copy link
Member

The current distinction is that fields are "things with an initializer" and methods are "things with a complete property descriptor".

@ljharb
Copy link
Member

ljharb commented Oct 30, 2018

@mbrowne matching getter/setter pairs should be coalesced before the decorator is called.

A field is certainly a property with an initializer; a method, to me, is a function-valued data property on the prototype.

@pabloalmunia
Copy link
Contributor Author

pabloalmunia commented Oct 30, 2018

Of course, strictly speaking, you can infer what type of element is decorated inspecting the descriptor without consulting the kind property. You can check if the descriptor has the get / set methods (accessor) , if the value property is assigned to a function (method) and in any other case it is a property (field).

I understand that kind is a fast, simple and clear way to know that it is decorating without needing to analyze more data. It's posible remove kind and the decorators work perfectly, but a good definition of kind is a help for programmers.

@pabloalmunia
Copy link
Contributor Author

Sorry, it's close by error.

@littledan
Copy link
Member

Nicolo explains it well #162 (comment) . I would add, methods are added before fields. You can actually make a decorator which creates a method whose value is not a function, and things like that, FWIW, but there is no syntax for that.

@nicolo-ribaudo
Copy link
Member

Just to specify, I'm not opposed to the kind: "accessor" proposal, but I think that it isn't that unexpected that accessors are labeled as methods.

@pabloalmunia
Copy link
Contributor Author

pabloalmunia commented Oct 30, 2018

@nicolo-ribaudo It's true, the method kind === 'method' is consistent with set / get because these accessors are methods, but it's not very simple way, in the practique development, for identify the kind of element decorated.

@mbrowne
Copy link
Contributor

mbrowne commented Oct 30, 2018

Another consideration is that if an "accessor" kind were added, the return value in decorator functions would have yet another way of specifying the same thing. If I recall correctly it's already possible to return an element descriptor that creates an accessor property with kind set to either "field" or "method", as long as the property descriptor includes a getter and/or setter.

@pabloalmunia
Copy link
Contributor Author

@mbrowne it's true, now the conversion from field to get/set with a decorators is this:

function check (descriptor) {
  if (descriptor.kind === 'field') {
    return {
      kind       : 'method',
      key        : descriptor.key,
      placement  : descriptor.placement,
      descriptor : {
        get () {
          console.log ('get', descriptor.key);
        },
        set (v) {
          console.log ('set', descriptor.key);
        }
      }
    }
  }
  return descriptor;
}

class Test {
  @check
  z = 10; 
}

const d = new Test ();
d.z     = 20;
let r   = d.z;

@nicolo-ribaudo
Copy link
Member

@mbrowne Currently there is only one way (using "method"). If you return a "field" with a get/set descriptor, it throws (step 16 of https://tc39.github.io/proposal-decorators/#sec-to-element-descriptor)

@littledan
Copy link
Member

There's a higher level issue here, which is that the decorator API isn't very developer-friendly in general. It is easy to use decorators and hard to write your own. We should consider publishing a utility library to make this easier, and checking whether a descriptor is an accessor could be part of that.

@mbrowne
Copy link
Contributor

mbrowne commented Oct 31, 2018

Sorry for my incorrect comment above...I guess it underscores the point that the API could be more intuitive.

One significant confusing thing about it is that it includes both the new concept of class/element descriptors and also ES5 property descriptors. While ES5 property descriptors have the benefit of familiarity and being closer to the metal, I still wonder if they need to be included in the public API at all or if they could be abstracted as an implementation detail. If a nested object were only needed to specify enumurable/writable/configurable, that would be fine. But when you get into initializer vs. property descriptor value, and which kind can be used with which property descriptor properties, that's where the API gets harder to learn.

@pabloalmunia
Copy link
Contributor Author

pabloalmunia commented Nov 5, 2018

For internal use, we are build two simple tables with the decorator descriptor parameter and return with some note when the behavior need explain.

parameter descriptor @decorator class @decorator method() @decorator field @decorator get field() set field()
`{`
  kind: "class" "method" "field" "method" 4
  elements: Array of member descriptors1 - - -
  key: - method name method name property name
  placement: - "prototype" || "static" "own" || "static" "prototype" || "static"
  initializer: - - function than return the initial value 2 -
  descriptor:{ -
    value: - method function - 3 -
    get: - - - getter function
    set: - - - setter function
    writable: - true true - 5
    configurable: - true true true
    enumerable: - false false false || true 12
  } -
}

1 element is an array of decorator descriptors, not to be confused with property descriptors.

2 initializer function is present only if the field is initialized into the class body: field = 10. Please, check if exist before call it.

3 when kind is field don't include the value into the descritor.value and the value, if exist, is returned by initializer function.

4 when the decorator is apply over a getter/setter kind is method and descriptor.get or descriptor.set has value.

5 when descriptor.get or descriptor.set has value, the property descriptor don't include writable value.

12 descriptor.enumerable is true if placement is "own".

return descriptor (optional) class method() field getter/setter
`{`
  kind: "class" "method" "field" "method"
  elements: Array of member descriptors 6 - - -
  key: - method name or decorators.PrivateName(name) 8 field name or decorators.PrivateName(name) 8 field name or decorators.PrivateName(name)8
  placement: - "prototype" || "static" || "own" "prototype" || "static" || "own" "prototype" || "static" || "own"
  extras: - Array of member descriptors 7Array of member descriptors 7Array of member descriptors 7
  initializer: - function than return the initial value - 10
  descriptor:{ - 9 9 9
    value: - method function - - 10
    get: - - - getter
    set: - - - setter
    writable: - true || false true || false - 10
    configurable: - true || false true || false true || false
    enumerable: - false || true false || true false || true
  } -
  finisher: callback 3 callback 3 callback 3 callback 3
}

6 element is an array of decorator descriptors, not to be confused with property descriptors. You can add (include new object descriptor in this array), remove elements (delete existed object descriptor from this array) or change elements (modify object descriptor elements).

7 extra is an array of decorator descriptors, not to be confused with property descriptors. You can add new member when a method or field is decorated.

8 key can be change from the original. It's a mandatory field. It the member is private (#name) a decorators.PrivateName(name) is included in this property.

9 descriptor is a mandatory field, but can be an empty object.

10 cannot include initializaer, descriptor.value or descriptor.writable when descriptor.get or descriptor.set are defined.

11 finisher function is a callback that is called at the end of class creation. It's optional.

@littledan
Copy link
Member

@mbrowne Do you have any particular ideas for how it could be changed? I don't see a clear way to "abstract" things without making it worse in some way or other, but maybe you do.

@littledan
Copy link
Member

@pabloalmunia Thanks, that's helpful. Could we add this table to the documentation for this proposal?

@pabloalmunia
Copy link
Contributor Author

@pabloalmunia Thanks, that's helpful. Could we add this table to the documentation for this proposal?

Off course. Please, review our contribution, we don't are expert in this kind of code, only an enthusiastic team with this new feature. For example, we don't kwon the behavior with private #method an #fields and it's not include in this table.

@littledan
Copy link
Member

FWIW the behavior for private is enumerated in TAXONOMY.md.

@pabloalmunia
Copy link
Contributor Author

FWIW the behavior for private is enumerated in TAXONOMY.md.

Thks. We update our tables with some little fixed after read this explain.

@littledan
Copy link
Member

@pabloalmunia Any way you could submit the table as a PR?

@mbrowne
Copy link
Contributor

mbrowne commented Nov 5, 2018

@littledan I think it might help to lift the property descriptor properties up to the top level, e.g.:

{
  key: 'foo',
  kind: 'method',
  placement: 'prototype',
  enumerable: false,
  writable: true,
  configurable: true,
  value: () => { console.log('foo') }
}

I think that the common expectation will be that you can use property descriptors like you would with Object.defineProperty, but that's not really the case. When using Object.defineProperty, you only have to worry about having a valid property descriptor. With decorators, you have to make sure you have a valid property descriptor, valid class element descriptor (that is, everything other than the property descriptor), and that you're using the right combination. And you can't even use value for anything except methods. Moving descriptor to the top level wouldn't resolve all of these issues but I still think it could help, at least by setting new expectations rather than exposing property descriptors directly.

But I don't want to overstate this criticism. Overall I don't think the current API is bad and I think the learning curve is pretty small. I'm just saying that the issues I mentioned above will probably trip some people up.

@pabloalmunia
Copy link
Contributor Author

@mbrowne this is a good approach, we have a lot of confusions talking about decorator descriptor than includes a property descriptor.

In the same way, and in my humble opinion the use of initializer and descriptor.value it is confuse. I understand the difference, but in a lot of case the field value can be inserted into value without conflict and the function is not necessary.

@pabloalmunia
Copy link
Contributor Author

@pabloalmunia Any way you could submit the table as a PR?

Done...

@littledan
Copy link
Member

I've been thinking about this more, and I'm increasingly convinced that it makes sense to use kind: "accessor" for accessors, just because it matches people's normal mental model, and doesn't hurt anything. Would anyone have concerns about this change?

@littledan
Copy link
Member

OK, we've adopted "accessor" in #199.

@littledan littledan mentioned this issue Jan 15, 2019
6 tasks
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

5 participants