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

Static decorators: one more built-in for MVP #260

Closed
Lodin opened this issue Mar 15, 2019 · 13 comments
Closed

Static decorators: one more built-in for MVP #260

Lodin opened this issue Mar 15, 2019 · 13 comments

Comments

@Lodin
Copy link
Contributor

Lodin commented Mar 15, 2019

@littledan, I have taken a look at the new proposal, and I would say that I like this proposal. It is less verbose than the previous one, uses fewer strings and objects in the implementation and looks more natural. However, I would like it to have comparable power with the deprecated stage 2. For now, we are getting back to the opportunities stage 1 proposed plus @initialize. Since I'm developing a project which entirely relies on decorators, it would be quite harmful to me.

So, here I would propose one more built-in decorator that could simplify the transition to a new proposal for stage 2 projects. I'm not sure about naming, so let's call it @inject. It is a reimplementation of { kind: 'initializer' } idea. It is a decorator similar to @register, but it happens during constructor after all the class fields are initialized but before the user-defined constructor runs. The implementation is simple:

@inject(f)
class C {}

is roughly equivalent to:

class C {
  constructor() {
    f.call(this);
  }
} 

As well as @register, it could be called multiple times in any cases.

I believe it could fill the gap this PR currently has.

@littledan
Copy link
Member

Thanks for writing this up. I was sort of alluding to this capability with @addInitializer in NEXTBUILTINS.md. Could you explain a little bit more about your use case? For example, should these initializers always be in the constructor, or do you ever need "static initializers"?

@Lodin
Copy link
Contributor Author

Lodin commented Mar 15, 2019

Yep, I've seen it there. Unfortunately, it is about the future which means that for my project that heavily relies on the instance initializers the usage of MVP would require specific hacks and won't be very clear.

Regarding use cases: I use a lot of class decorators that creates a system that runs for a particular class. So I need to create a lot of private class properties that contain an internal state of the system. Using the previous proposal I just add new fields to the elements directly, but static decorators don't allow me doing that. So I need a tool to do it another way.

I definitely can do the same with using a support class extending the user's one, but I would prefer to avoid it because it changes the prototype chain which could be unexpected for a user. Hmm... Maybe it is the only argument I can bring here. However, I still believe that having a tool that doesn't change the prototype chain is excellent.

Regarding static initializers: well, I don't think we really need it. Everything can be done via the @wrap decorator that returns the same class it receives but produces side effects along with it. A bit tricky, but should work like a miracle. It also solves the issue with @register order I raised in the PR. So I would suggest to keep @inject as simple as possible. Just a function that runs in the constructor.

@Lodin
Copy link
Contributor Author

Lodin commented Mar 21, 2019

I performed a couple of experiments and want to say that @inject or another similar solution is quite essential for the new proposal because it is the only way to run something before the class constructor is invoked.

So, the best solution that allows sneaking into the constructor is the following approach:

decorator @sneak {
  @wrap(target => function () {
    const instance = new target();
    instance.a = 10;
    instance.b = 'foo';
    return instance;
  })
}

It doesn't leave any trace in the prototype chain and gets an ability to execute almost everything after the constructor. But I didn't find a way to invoke anything before the constructor.

It means that adding any property during the instantiation makes this property inaccessible in the constructor. E.g., in the @corpuscule/form I have a form instance that should be created before the user constructor and be accessible for the user in the constructor. However, I cannot do it using only the @wrap decorator.

BTW, if anyone knows the technique, I would be glad to hear.

@nicolo-ribaudo
Copy link
Member

You could use something like this:

decorator @injectInitialization(init) {
  @wrap(target => {
    const proto = target.__proto__;
    class Injector {
      constructor(...args) {
        const instance = Reflect.construct(proto, args, new.target);
        init.call(instance);
        return instance;
      }
    };

    return function (...args) {
      target.__proto__ = Injector;
      const instance = new target(...args);
      target.__proto__ = proto;
      return instance;
    };
  })
}

Demo:

function inject(init, target) {
    const proto = target.__proto__;
    class Injector {
      constructor(...args) {
        const instance = Reflect.construct(proto, args, new.target);
        init.call(instance);
        return instance;
      }
    };

    return function (...args) {
      target.__proto__ = Injector;
      const instance = new target(...args);
      target.__proto__ = proto;
      return instance;
    };
  }


class A {
  constructor(p) { console.log("A", p) }
}

class B extends A {
  constructor(p) { super(p); console.log("B", p, this.x); }
}

var C = inject(function() { this.x = 4 }, B)

new C(3);

@nicolo-ribaudo
Copy link
Member

Oh, I just realized that my example only works when decorating derived classes.

@Lodin
Copy link
Contributor Author

Lodin commented Mar 21, 2019

@nicolo-ribaudo It looks entirely like magic here 😄
But wouldn't it harm the optimization V8 performs? I've seen a scary warning on the MDN's Object.setPrototypeOf page.

@littledan
Copy link
Member

I am currently thinking that we should just generalize initialize to do inject when it's not used on a field. Would that work for you?

@nicolo-ribaudo
Copy link
Member

Yeah, where the initializer function is called with an interface like

@initialize(([key [, value]) => void)

@littledan
Copy link
Member

Exactly! And if you use initialize on a field multiple times, the outer instance gets called with a single argument.

@Lodin
Copy link
Contributor Author

Lodin commented Mar 22, 2019

@littledan

I am currently thinking that we should just generalize initialize to do inject when it's not used on a field. Would that work for you?

Yep, that would be great!

And if you use initialize on a field multiple times, the outer instance gets called with a single argument.

Hmm, I'm not sure I get the idea. Could you explain it a bit?

@nicolo-ribaudo
Copy link
Member

It's to avoid re-initializing already initialized fields. The first initializer will install the value on the instance, if subsequent initializers need it they can get it with normal reflection.

@littledan
Copy link
Member

How does the current decorators proposal do for your use cases?

@littledan
Copy link
Member

Note that the current proposal doesn't really give you this kind of @inject capability, but you could sort of get the same kind of thing out of "block decorators" from EXTENSIONS.md.

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