tc39 / proposal-decorators

Decorators for ES6 classes
https://arai-a.github.io/ecma262-compare/?pr=2417
2.76k stars 106 forks source link

auto-accessors: difference between `value.get` and `context.access.get` #537

Closed frehner closed 4 months ago

frehner commented 4 months ago

Forgive my ignorance here, but hoping to learn more.

It's slightly unclear to me from reading the README what, if any, difference there is between a decorated auto-accessor's value.get/value.set and context.access.get/context.access.set methods.

https://github.com/tc39/proposal-decorators?tab=readme-ov-file#class-auto-accessors

type ClassAutoAccessorDecorator = (
  value: {
    get: () => unknown;
    set(value: unknown) => void;
  },
  context: {
    ...
    access: { get(): unknown, set(value: unknown): void };
    ...
  }
)

Is there any difference? Is there a preference for one over the other? I've looked over the spec changes but I'm not quite yet to a place where I can make sense of it yet. Thanks! 🙂

pzuraq commented 4 months ago

So, the value passed into the decorator is the value being decorated, and the value accessed via access.get and access.set is the final value. For instance:

class Foo {
  @foo
  @bar
  accessor baz = 123;
}

The final value of baz is essentially foo(bar({ get, set })), where the get and set functions are the original getter/setter on the class, right?

Well, if the bar decorator wanted to access the value via the final decorated getter, there isn't a way it could do that without access. It has no knowledge of @foo, it doesn't ever get access to those functions, so there's no way for it to provide access to the final value, it can only expose access to the intermediate value.

access.get is essentially like calling Reflect.get(obj, key) for this value, but with one key difference - it supports private fields/methods, which cannot be handled via Reflect.get. This essentially prevents ordering issues, where the last decorator is fighting to be able to have access in cases where that's necessary.

frehner commented 4 months ago

So, the value passed into the decorator is the value being decorated, and the value accessed via access.get and access.set is the final value. For instance:

class Foo {
  @foo
  @bar
  accessor baz = 123;
}

The final value of baz is essentially foo(bar({ get, set })), where the get and set functions are the original getter/setter on the class, right?

👍

Well, if the bar decorator wanted to access the value via the final decorated getter, there isn't a way it could do that without access. It has no knowledge of @foo, it doesn't ever get access to those functions, so there's no way for it to provide access to the final value, it can only expose access to the intermediate value.

access.get is essentially like calling Reflect.get(obj, key) for this value, but with one key difference - it supports private fields/methods, which cannot be handled via Reflect.get. This essentially prevents ordering issues, where the last decorator is fighting to be able to have access in cases where that's necessary.

Hm, I'm having a harder time understanding this part, sorry! Using the example you wrote up before, what would change if instead of set.call(this, removed) we did context.access.set.call(this, removed)?

I'm attempting to run something very similar to that using get in a babel repl, but it fails. (I'm not sure if I'm doing something wrong, or if it's just not a complete implementation yet).

pzuraq commented 4 months ago

So, if you called context.access.set.call(this, removed), it should fail, because that will cause an infinite loop. It's basically the same as doing this:

class Foo {
  set bar(value) {
     this.bar = value;
  }
}

It's the same as call foo.bar = value on an instance. The main reason for this API is to provide access to other code that may want to read the value of a decorated element, for instance if you want to expose access to a private element to check its state, but ONLY in tests. Does that make sense?

frehner commented 4 months ago

Ah ok, interesting. So access shouldn't be used in getters/setters, but could be passed to other code/places to get the final value. I think I understand now.

Thanks for being patient with me and walking me through that. I appreciate it.

pzuraq commented 4 months ago

No problem! I like digging in and explaining things, it helps to show where the mental model is at and what we need to focus on for documentation and education 😄

frehner commented 4 months ago

So, the value passed into the decorator is the value being decorated, and the value accessed via access.get and access.set is the final value. For instance:

class Foo {
  @foo
  @bar
  accessor baz = 123;
}

The final value of baz is essentially foo(bar({ get, set })), where the get and set functions are the original getter/setter on the class, right?

Something that has been throwing me off is that the behavior of this is somewhat surprising in practice; the body of @bar will execute before the body of @foo (as expected).

However, the getters/setters of @foo will execute before the getters/setters of @bar. In other words, the getters/setters execute top-to-bottom (or left-to-right). Example:

class Temp {
  @removeNums @removeCaps accessor thing
}

function removeNums(value, context){
  console.log('nums body')
  return {
    set(val) {
      console.log('nums setter', val)
      const removed = val.replace(/[0-9]/g, '')
      value.set.call(this, removed)
    },
    get() {
      console.log('nums getter')
      return value.get.call(this)
    }
  }
}

function removeCaps(value, context){
  console.log('caps body') 
  return {
    set(val) {
      console.log('caps setter', val)
      const removed = val.replace(/[A-Z]/g, '')
      value.set.call(this, removed)
    },
    get() {
      console.log('caps getter')
      return value.get.call(this)
    }
  }
}

const temp = new Temp()
temp.thing = '1Ab'
console.log(temp.thing)

will log

caps body
nums body
nums setter 1Ab
caps setter Ab
nums getter
caps getter
b
pzuraq commented 4 months ago

Yup, that's correct! Decoration happens in reverse order, which results in @foo wrapping @bar, which wraps the property. The result is necessarily the inverse, same as if you decorated a function or a method.