Skip to content

New decorator proposal #62

@silkentrance

Description

@silkentrance

This is building upon #23 and #61/#65. This can be combined with #66. And it might also be a provable solution for #68. See also #67 where I try to establish a common nomenclature of what types of functions/objects we are dealing with.

Here @lukescott presented a provable solution based on a suggestion made by @jayphelps that would require introduction of another built-in object that will be passed to the decorator function upon decoration.

With the introduction of the yield statement and along with it generators, two new built-in objects were introduced to the language, namely Generator and GeneratorFunction, not to mention the newly introduced Symbol.

So what about adding yet another built-in object, say

interface DecorationDescriptor
    constructor(target : Class | PrototypeObject, optional attr : String, optional descriptor : DescriptorObject)
    readwrite property target : Class | PrototypeObject
    immutable attr : String
    readwrite property descriptor : DescriptorObject

where immutable means that the property is not configurable and readonly and 
readwrite means that the property is not configurable but can be written to.

Additional properties for testing the 'nature' of the descriptor, say decoratesClass : Boolean, could be added either by default or simply by extending the native object's prototype.

And, similar to Symbol, it should not be instantiated using new but rather by calling DecorationDescriptor(target, attr, descriptor), to "prevent" the casual user from using it.

Instances of this would then be passed to the decorator function upon decoration which then could use a simple instanceof check to determine whether it is being used as a decorator or whether it was called with a different set of arguments, for example in cases where we have parameterizable decorators that will return the actual decorator when being called.

Having this built-in object, will reduce the signature of the decorator function to just

function (descriptor : DecorationDescriptor) : DecorationDescriptor | void(0)

and will help eliminate the ambiguity that goes along with testing for typeof arguments[0] == 'function' or typeof arguments[0] == 'object' and so on from a decorator developer's perspective, considering the sort-of proposal found in #23.

Existing transpilers, or even native engine code, would then need to create a new instance of proposed decoration descriptor, whatever the built-in object's final name would be, and pass it to the decorator function upon decoration.

All of this, of course, goes hand in hand with the proposal made in both #23 and #61, namely eliminate the need for having to use a call statement when not requiring any additional parameters for an otherwise parameterizable decorator, e.g.

function pdecorator(options)
{
     // handle options
     return function decoratorImpl(target, attr, descriptor) {....};
}

would become (simplified example here!)

function pdecorator(optionsOrDescriptor)
{
    let descriptor;
    let options = optionsOrDescriptor;
    if (optionsOrDescriptor instanceof DecorationDescriptor)
    {
        descriptor = optionsOrDescriptor;
        options = makeDefaultOptions({});
    }
    else
    {
        options = mergeUserOptions(options);
    }

    const decoratorImpl = function _decoratorImpl(descriptor)
    {
        // make use of options here...
    }

    if (descriptor instanceof DecorationDescriptor)
    {
         return decoratorImpl(descriptor);
    }
    return decoratorImpl;
}

With that in place, the decorator could then be used as follows

Class Decorator Use

1. @pdecorator
class Foo {}

2. @pdecorator()
class Foo {}

3. @pdecorator({option1:'Bar'})
class Foo {}

Desugaring

1. var Foo = (function () {
  function Foo() {
  }

  var cdd = DecorationDescriptor(Foo);
  var cdd2 = pdecorator(classDecorationDescriptor) | classDecorationDescriptor;
  var F = cdd2.target;
  return F;
})();

2. var Foo = (function () {
  function Foo() {
  }

  var cdd = DecorationDescriptor(Foo);
  var cdd2 = pdecorator()(classDecorationDescriptor) | classDecorationDescriptor;
  var F = cdd2.target;
  return F;
})();

3.  var Foo = (function () {
  function Foo() {
  }

  var cdd = DecorationDescriptor(Foo);
  var cdd2 = pdecorator({option1:'Bar'})(classDecorationDescriptor) | classDecorationDescriptor;
  var F = cdd2.target;
  return F;
})();

and so on.

Considering #68, the generated code can now safely assume that the decorator function returned an instance of DecorationDescriptor or did not have any return result.

The latter of course implies that the decorator is able to modify the descriptor in place, given the above interface proposal.

Caveats

Misuse

Of course, this will fail if the user decides to pass in a custom instance of DecorationDescriptor, e.g.

@pdecorator(DecorationDescriptor(function () {}))
class Foo {}

But then again, nothing is safe from being misused. However, this might also be a nice feature to have for frameworks that need to augment existing classes after that they have been declared.

Breaking Change

Existing decorators need to be reimplemented.

Increased Complexity of the Decorator

Decorators, even those that are not parameterizable, need to implement additional guards and also need to be implemented similarly to those that are parameterizable. But I do not see anything wrong in that, e.g.

function simpledecorator(descriptor)
{
     const decoratorImpl = function _decoratorImpl(descriptor) {};

     if(descriptor instanceof DecorationDescriptor)
     {
         // decorate ...
         return decoratorImpl(descriptor);
     }
     return decoratorImpl;
}

and its use

@simpledecorator()
class Foo {
     @simpledecorator
     get prop() {}

     @simpledecorator()
     doSomething() {}
}

would be streamlined with that of parameterizable decorators. And, if the author of the simpledecorator decides to make it a parameterizable one, there will be no change in behavior unless these parameters are mandatory. So win-win for everyone, or is it not?

Increased Complexity in Transpiler / Engine

The engine needs to implement a new built-in object. Given that engines do not provide for such, the transpiler needs to provide a substitute for that and must include it with code that makes use of the decorator feature. This leaves some questions though, especially with testing of decorators and so on and where the built-in object is not yet available.

As for the transpiled code, the transpiler needs to generate code that will create instances of the proposed new built-in object and pass these instances to the decorator function instead of just passing target, attr, descriptor. So there is only a slight increase in complexity there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions