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

Proposal: Allow client code to intercept the decoration process #65

Open
Download opened this issue Mar 22, 2016 · 35 comments
Open

Proposal: Allow client code to intercept the decoration process #65

Download opened this issue Mar 22, 2016 · 35 comments

Comments

@Download
Copy link

TL;DR;
Allow client code to intercept the decoration process by defining a well-known Symbol.decoratorCall. When the runtime detects a property on a decorator with this symbol as it's name and a function as it's value, the runtime will invoke that function i.s.o. the decorator function itself to perform the decoration.

Background

There has been a lot of discussion around the way decorators are used and how the spec allows us to write code for these scenarios. Mostly, there are two ways to use decorators that are used a lot:

@deco  // <-- no parentheses
class Subject {}
@deco('argument')
class Subject

So far so good, but what happens if the decorator accepts an optional argument? We find that whether we supply parentheses or not makes a difference. In other words, these two uses of decorators are not equivalent:

@deco
class Subject
// not equivalent to
@deco()
class Subject

The big difference is that in the first scenario deco is invoked as a decorator and is expected to return the decoration target (e.g. the class in this example), whereas in the second scenario, deco will first be called without any arguments and is expected to return a function, which will then be invoked as the decorator. We will call this a decorator factory.

Many authors have expressed the desire to have @deco and @deco() have the same outcome: The target is decorated with deco without any arguments. This can be achieved with an if inside the decorator that uses duck typing to determine whether the function is invoked as a decorator or as a decorator factory:

function deco(arg) {
  if (typeof arg == 'function') {
    // decorator
    arg.decorated = true;
    return arg;
  } else {
    return target => {
      target.decorated = arg;
      return target;
    }
  }
}

This function will handle all scenarios described above. I will refer to functions that work this way as 'universal decorators'. They are convenient. But this code contains some problems:

  1. The check in the if is brittle. What if we want to write a class decorator that optionally accepts a function as an argument? In some cases, it may be impossible to distinguish between decorator and decorator factory invocations.
  2. This code contains much redundancy. Basically we are writing the decorator twice: once for the decorator invocation and once for the decorator factory invocation. We can of course factor out some code into a sub function but it all makes our decorator needlessly complex.

The discussion around this topic has spawned some interesting ideas for solutions, the most extreme of which would break all existing decorators. This proposal attempts to formulate a solution to these problems that is fully backwards compatible.

Hooking into the decoration process

The if in our universal decorator example above is really what is causing us all our grief. It splits our code in two and worse, the condition it is checking is brittle and will often have to change depending on what arguments our function accepts. What if we could get rid of this if altogether?
Turns out we could. And best of all, we could do it without breaking backward compatibility. As a bonus we would be creating a generic mechanism that could support other scenarios besides writing universal decorators. We can allow authors to hook into the decoration process itself.

Symbol.decoratorCall

In Javascript, functions are objects. Which means they can have properties. We can use this to allow authors to set a property on a decorator function in such a way that they can trap the decoration process. Given a well-known symbol Symbol.decoratorCall, authors could set a property on their decorator using this symbol to set a function to be called by the runtime instead of the decorator function itself. As authors, we now have an extra tool that can help us rewrite our universal decorator to something much simpler:

function deco(arg = true) {
  return target => {
    target.decorated = arg;
    return target;
  }
}
deco[Symbol.decoratorCall] = function(...args) {
  return deco()(...args);
};

Notice how the function assigned to the Symbol.decoratorCall property is completely generic. We as authors can take advantage of this fact to implement our own tooling for universal decorators:

function universalDecorator(factory) {
  factory[Symbol.decoratorCall] = function(...args) {
    return factory()(...args);
  }
  return factory;
}

Then, we could write only the decorator factory and let the tooling turn it into a universal decorator for us:

const deco = universalDecorator(function deco(arg = true) {
  return target => {
    target.decorated = arg;
    return target;
  }
});

If we ever get decorators on functions, we can make it even nicer :)

Conclusion

Although this proposal was born out of the desire to be able to write universal decorators more easily and to avoid duck typing, I think that being able to intercept the decoration process will actually open up options to deal with other issues as well.

Advantages:

  1. Fully backwards compatible. This change should not break any existing decorators.
  2. Meshes well with how other runtime features can be hooked into using Symbol. E.g. Symbol.iterator which can make our object work with the for ... of loop.
  3. Opt-in. Only people that care about universal decorators need to do something to make use of this proposal. All other authors could happily ignore it and never be the wiser.

Disadvantages:

  1. Opt-in. From the perspective of authors using decorators, they still need to figure out per decorator whether the use of parentheses matters for that decorator, and which form to use.
  2. Requires a new well-known symbol.
@Download
Copy link
Author

@silkentrance @lukescott @isiahmeadows @alexbyk @jayphelps
Would you guys support this proposal?

@jayphelps
Copy link
Contributor

Will stew on it. One minor typo:

deco.call(ctx, ...args) {
  return deco().call(ctx, ...args);
}

// should be

deco.call = function (ctx, ...args) {
  return deco().call(ctx, ...args);
};

@dead-claudia
Copy link

@Download Maybe Symbol.call or similar? Otherwise, you're interfering with Function.prototype.call. Other than that, I'm fine with it, as long as there's a runtime check to ensure that method exists before falling back to the default of just using the function directly.

// Original
class Foo {
  @decorator
  bar() {}
}

// My proposed desugaring, where $variables are purely internal:
class Foo {
  bar() {}
}

let $hasCall = Object.prototype.hasOwnProperty.call(decorator, Symbol.call);
let $desc = Object.getOwnPropertyDescriptor(Foo.prototype, "bar");
let $ref = $hasCall ?
    decorator[Symbol.call](Foo.prototype, "bar", $desc) :
    decorator(Foo.prototype, "bar", $desc);
Object.defineProperty(Foo.prototype, "bar", $ref !== undefined ? $ref : $desc);

@lukescott
Copy link

call is a built in function. Overriding it in this fashion is confusing. Symbol.call is for making a class callable without 'new'. At that point why not make it a class?

As I've said before I believe backwards compatibility is an unreasonable constraint just because people jumped the gun on a stage 0/1 proposal. I would rather do this right then be limited by bad decisions.

@dead-claudia
Copy link

@lukescott I'm not that tied to Symbol.call (I said "or similar"). I just want it to be something that doesn't conflict with Function prototype methods (e.g. Function.prototype.call). And symbols are a great way to avoid this.

@Download
Copy link
Author

@isiahmeadows
Can you enlighten me on how using call in this way conflicts with Function.prototype.call? It's not like we are overriding .call on every function.

Note that we can already override .call on any function right now (and indeed that is what my example does). So again, this is about letting the client code decide whether to hook into the process this way or not. All the runtime needs to do is give the client code this ability.

I'm trying to look for a scenario where just always invoking .call i.s.o. checking whether the function hasOwnProperty first would make a practical difference. Remember, this would only happen during a decoration call (e.g. what the runtime does when it encounters @), so there won't (shouldn't) be much code affected in any case. I think we would be looking at the edge case of an edge case, so to say. Do you have a scenario in mind?

@lukescott The decorators proposal is defining a whole new symbol with @. If we end up specifying it's behavior as 'evaluates the expression to the right hand side of the @ symbol and if it's a function, invokes it's call method', I (personally) don't think that's very confusing at all. It does not change the definition of what call is or what it does in any way.

Again, overriding call is already possible right now and this will remain so in the foreseeable future. Some people don't like this part of Javascript where anything goes, but it is what it is. My proposal does not touch that in any way. At the most it could be seen as promoting a bad practice, but I think that's a pretty subjective viewpoint.

@jayphelps
Fixed the typo, thanks for pointing it out!

@silkentrance
Copy link

@Download please see the proposal #66 and you might want to comment on that.

@dead-claudia
Copy link

@Download I think I see where you're going with this, but what's this? Also, FWIW, I missed the key detail that you're being careful in that it's always similar to Function.prototype.call.

For others that overlook some of the more minute details of this idea, it's the difference between this (%variables are internal):

// Original
class C {
  @decorator
  method() {}
}

// Transpiled now
class C {
  method() {}
}

var %func = decorator
var %desc = Object.getOwnPropertyDescriptor(C.prototype, "method")
%desc = %func(C.prototype, "method", %desc) || %desc
Object.defineProperty(C.prototype, "method", %desc)

// Proposed here
class C {
  method() {}
}

var %func = decorator
var %desc = Object.getOwnPropertyDescriptor(C.prototype, "method")
%desc = %func.call(C.prototype, "method", %desc) || %desc // Here's the difference
Object.defineProperty(C.prototype, "method", %desc)

My main concern is with TypeScript, which might result in type conflicts once variadic generics are implemented (it's a beast).

interface Function<R, T, ...A> {
  call(thisArg: T, args: [...A]): R;
}

type MethodDecorator<T> = (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void

interface Decorator<T, ...A> extends (...args: [...A]) => MethodDecorator<T> {
  // inherited with [...A] = [Object, string | symbol, TypedPropertyDescriptor<T>]:
  // call(thisArg: any, arg0: Object, arg1: string | symbol, arg2: TypedPropertyDescriptor<T>): MethodDecorator<T>
  call(thisArg: any, arg0: Object, arg1: string | symbol, arg2: TypedPropertyDescriptor<T>): TypedPropertyDescriptor<T> | void
}

That return type will fail to check, because the types are wholly incompatible. That's my greater concern, since decorators are of equally large interest to the TypeScript community (they're looking into integrating them more deeply than even JS proper, if you look at their roadmap).


To be honest, TypeScript compatibility is probably the only reason I can't support this idea.

@Download
Copy link
Author

@isiahmeadows Yes the way your example transpiles is exactly what I had in mind.
About the TypeScript compatibility I have to admit I never though about it, because I've never programmed with TypeScript.

Would there be a way around the issue you are describing?

what's this?

If you mean what this would be bound to inside the decorator function, I reckon we should make sure it behaves the same as it does now.

@dead-claudia
Copy link

@Download

Would there be a way around the issue you are describing?

Why I suggested a symbol. TypeScript understands those perfectly, and it's very easy to check for the existence of such a symbol (hasOwnProperty check). I personally suggested Symbol.call, but I have no personal attachment to the name. Babel and TypeScript can always generate a new helper to simplify this.

This also is more behaviorally and logically consistent with the rest of ES2015+, where magic methods and values are all denoted by symbols (like Symbol.iterator and Symbol.hasInstance).

What it would desugar to, then, is this:

// Original
@Class
class C {
  @Method
  foo() {}
}

// New
function %decorateClass(target, decorator) {
  if (Object.prototype.hasOwnProperty.call(decorator, Symbol.call)) {
    decorator = decorator[Symbol.call]()
  }
  return decorator(target) || target
}

function %decorateProperty(target, property, decorator) {
  if (Object.prototype.hasOwnProperty.call(decorator, Symbol.call)) {
    decorator = decorator[Symbol.call]()
  }
  const desc = Object.getOwnPropertyDescriptor(target, property)
  Object.defineProperty(target, property, decorator(target, property, desc) || desc)
}

class C {
  method() {}
}

%decorateClass(C, Class)
%decorateProperty(C.prototype, "foo", Method)

If you mean what this would be bound to inside the decorator function, I reckon we should make sure it behaves the same as it does now.

This is pretty much what I expected. I was just double-checking.


// I like the idea of this:
function universal(wrapper) {
  wrapper[Symbol.call] = wrapper
  return wrapper
}

const Injectable = universal((opts = {}) => (target, prop, desc) => {
  // do things...
})

// Or if you *really* need a class...and no, inheritance doesn't work.
function decorator(D) {
  function decorator(opts = {}) {
    const d = new D(opts)
    d[Symbol.call] = function () {
      return (...args) => this.apply(...args)
    }
    return d
  }
  Object.setPrototypeOf(decorator, D)
  decorator.prototype = D.prototype
  decorator[Symbol.call] = () => new D()[Symbol.call]()
  Object.defineProperty(decorator, "name",
    Object.getOwnPropertyDescriptor(D, "name"))
  return decorator
}

@decorator
class Injectable {
  constructor(opts) {
    this.opts = opts
  }

  apply(target, prop, desc) {
    // do things...
  }
}

@Download
Copy link
Author

@isiahmeadows Those are some cool ideas.

I have nothing against using a symbol. I just did not see any need for it. It seems like a relatively small change. As long as the end result is that we can intercept the decoration call I would be very happy with it.

A bit off-topic, but why would you write it like this:

Object.prototype.hasOwnProperty.call(decorator, Symbol.call)

...and not like this?

decorator.hasOwnProperty(Symbol.call)

EDIT: had some trouble finding relevant info on Symbol.call... Symbol and call are both used a lot in other contexts. :) The term 'well-known' helps. Anyway I think I found the relevant section of the draft spec, but alas Symbol.call is not in the list... Does anyone have a better link?

@dead-claudia
Copy link

@Download This. 😄

// Totally valid JS:
decorator.hasOwnProperty = function (what, the, heck) {
  return what + the + heck
}

decorator.hasOwnProperty("somethingThatDoesn'tExist") === "somethingThatDoesn'tExistundefinedundefined"

Or in all seriousness, the object can define its own variant, and thus it no longer actually checks what you think it is.


As for Symbol.call being used, I think it might be used in some sort of ES strawman somewhere, but an initial Google search is failing me here. And no, this symbol doesn't yet exist in anything stage 1 or higher. But I'm suggesting adding a new well-known symbol @@call, which would be referred to by Symbol.call.

@Download
Copy link
Author

@isiahmeadows

And no, this symbol doesn't yet exist in anything stage 1

That explains why I can't find it! :)

I assumed it existed because of @lukescott 's comment that

Symbol.call is for making a class callable without 'new'

Do you have a link where I can learn more about that maybe Luke?

@dead-claudia
Copy link

@lukescott If you're referring to the call constructor proposal, that's now using specific syntax instead.

class Foo {
  call constructor(whatever) {
    // do stuff...
  }
}

@silkentrance
Copy link

@Download @isiahmeadows The problem I have with this approach is that the author of the decorator must define the decorator using two distinct steps.

In addition, Isiah just proposed adding the well known symbol @@call.

With #62, the definition of a decorator is far more simple and with the introduction of the DecorationDescriptor, even #68 can be fixed, always requiring the decorator to return an instance of a well known object, namely the DecorationDescriptor. This would also fit well into the TypeScript world...

@dead-claudia
Copy link

Edit: add some explanation

@silkentrance 👎 for adding a whole new type to the language. Like we need more globals. I don't think we need an extra type to nominally check. My idea of a @@call isn't that far off of an instanceof check.

And for yours being conceptually simpler, I disagree. IMHO, 2 "steps" is simpler than 1 here.

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

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

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

// Mine
function pdescriptor(options = makeDefaultOptions({})) {
    options = mergeUserOptions(options);

    // decorator implementation
    return (target, prop, descriptor) => {
        // make use of options here...
    };
}

pdescriptor[Symbol.call] = pdescriptor;

For a proper comparison, here's what I see.

Yours:

  1. Define a pdescriptor function that accepts an argument and does the following:
    1. Declare descriptor and options.
    2. If the argument is a DecorationDescriptor object,
      1. Let descriptor be that argument, and options be the default options.
      2. Otherwise, merge in the user options to options with that argument.
    3. Define the decorator implementation.
    4. If descriptor is set,
      1. Return the result of calling the decorator implementation.
      2. Otherwise, return the decorator implementation function.

Mine:

  1. Define a pdescriptor function that optionally accepts an argument and does the following:
    1. If an argument was passed,
      1. Let options be that argument.
      2. Otherwise, let options be the default options.
    2. Define and return the decorator implementation function.
  2. Set the Symbol.call property of pdescriptor to itself.
    • Note that this, when called, will never be called with arguments.

@dead-claudia
Copy link

And I also just realized an added benefit for this: you could actually properly use curried functions as decorators by using @@call. It's not limited to just cases like Injectable vs Injectable().

function curry(f, len = f.length) {
  if (len === 0) return f
  function result(...args) {
    if (args.length === 0) return result
    if (args.length >= len) return f(...args)
    return curry((...rest) => f(...args, ...rest), len + args.length)
  }
  // Fully apply this if using it as a decorator
  result[Symbol.call] = () => f
  return result
}

const decorator = curry((name, opts, target, prop, desc) => {
  if (prop == null) {
    // handle class
  } else {
    // handle property
  }
})

@decorator("Foo class")({some: "options"})
class Foo {}

/cc @Download @silkentrance

@lukescott
Copy link

As far as overriding function.call, I vote no. This changes the expectation of what call does. The expectation is that call is going to call function with a specific this. What @Download is proposing is call now calls a different function. This breaks that expectation.

Using Symbol.call, or Symbol.decoratorCall, etc... instead is better. And the idea of having a separate function for the factory and decorator is, IMO, desired. I just don't agree with the way it is done. Why not do something like this instead?

var decorator = {
  decorate: function(target, prop, desc) {
    // ...
  }
};

function decoratorFactory(args) {
  return {
    decorate: function(target, prop, desc) {
      // ...
    }
  };
}

It doesn't have to be exactly like the above, but what I'm getting at is the decorator doesn't have to be a function.

As far as "backwards compatibility" is concerned, you could easily do a typeof check and support the old style and display a warning to upgrade:

// code used by compiler (babel,etc.) runtime which can be simplified at a later date
function decorateHelper(decorator, target, prop, desc) {
  if (typeof decorator === "function") {
      console.warn("old style decorator used. will be removed in future. please upgrade!");
      return decorator(target, prop, desc);
  }
  return decorator.decorate(target, prop, desc);
}

I love being able to use separate functions instead of ifs, as this issue suggests, so I would like to extend the above to something like:

var decorator = {
  decorateMethod: function(descriptor, prop, protoOrClass) {
   // ...
   return descriptor;
  },
  decorateClass: function(Class) {
    // ...
    return Class;
  }
};

Doesn't have to be exactly like that, but the idea is that there is a different function depending on the context in which the decorator is used. decorateMethod or decorateClass could be symbols.

@dead-claudia
Copy link

@lukescott That's not much different than my/@Download's current proposal, if you're relying on methods (as this proposal does). The only difference is more verbosity for less duck typing.

// Yours
function decorator(opts = {}) {
  return {
    decorateMethod: (desc, prop, proto) => {},
    decorateClass: Class => {},
  }
}

decorator.decorateMethod = (...args) => decorator().decorateMethod(...args)
decorator.decorateClass = (...args) => decorator().decorateClass(...args)

// Mine
function decorator(opts = {}) {
  return (target, prop, desc) => {}
}

decorator[Symbol.call] = decorator

@Download
Copy link
Author

@isiahmeadows You have won me over. I especially like the fact that Symbol.call does not yet exist. We could have a debate on what the best symbol name would be (e.g. we could opt for decoratorCall i.s.o. call to narrow the scope and leave call for other, more generic mechanisms that may come up in the future) but I think that you have made a strong point for specifying a symbol for this. It seems to make sense when we look at the other symbols that are currently specified. E.g. things like @@iterator and @@toPrimitive which seem to offer the same kind of interception / customization options for authors.

The currying example you showed is another cool idea! I think it shows that having a mechanism to allow authors to intercept the decoration mechanism opens up a whole range of useful options.

@lukescott
Copy link

@isiahmeadows The difference is I'm suggesting the decorator should be an object, not a function. And for "backwards compatibility" a runtime typeof check determines whether the decorator is of the new format or not. The intention is to allow old decorators to work for a time, and eventually expect it to be the new way before it gets implemented into browsers. -- I'm arguing for a single consistent way to define decorators. Leaving it a function and saying "if Symbol.decoratorCall is defined use that, if not fall back to the function" is a bit confusing. Having two ways to do the same thing is a bit confusing.

The decoratorMethod and decoratorClass thing was an extension. To allow a decorator to work in both contexts without an if statement.

@Download Download changed the title Proposal: Make runtime invoke decorators via their call method Proposal: Allow client code to intercept the decoration process Mar 28, 2016
@Download
Copy link
Author

@isiahmeadows I updated this proposal to reflect your remarks on using a well-known symbol i.s.o. overriding the call method of the function.

@dead-claudia
Copy link

@lukescott That's a whole separate proposal, mostly unrelated to this (or even the original #23). It deals with the fundamental type of a decorator in general, where this deals with @decorator foo() {} vs @decorator() foo() {}.

@Download Okay. 😄

@silkentrance
Copy link

@isiahmeadows the proposal in #62 was born from an idea by @jayphelps that one needs to duck type the existing arguments to find out whether one is decorating a class, or an instance property or method and so on. Therefore the extra built-in type.

And as far as 'currying' goes, the proposal does not set any limits to it 😃

But we should stay on topic here and discuss this over at #62 if you will.

Regardless of that, introducing a new Symbol.decoratorCall is not very different and requires both the engine and the transpilers to be aware of such a built-in Symbol.decoratorCall. Whereas in opposite of the DecorationDescriptor the new Symbol.decoratorCall would be less flexible as one is unable to determine the exact nature of the decoration, leaving us again with the duck typing process finding out what it is that we need to decorate. Which can actually be overcome by the proposal found in #62. And since this is an alternate proposal to that, this can also be considered taken care of, with less hassle on both the engine/transpiler part and the implementation of the decorator.

@dead-claudia
Copy link

@silkentrance

@isiahmeadows the proposal in #62 was born from an idea by @jayphelps
that one needs to duck type the existing arguments to find out whether one
is decorating a class, or an instance property or method and so on.
Therefore the extra built-in type.

Pretty much what I said about it not being fully relevant to this.

As for the duck typing, I don't have as much of a problem with it. It's
rare in my experience for decorators to apply to more than one type
(method/property or the class itself), so I can write and document them
accordingly where necessary. And since this is the common case, anything
other than a plain function IMHO is just adding meaningless boilerplate.

On Thu, Mar 31, 2016, 14:55 Carsten Klein notifications@github.com wrote:

@isiahmeadows https://github.com/isiahmeadows the proposal in #62
#62 was born from
an idea by @jayphelps https://github.com/jayphelps that one needs to
duck type the existing arguments to find out whether one is decorating a
class, or an instance property or method and so on. Therefore the extra
built-in type.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#65 (comment)

@silkentrance
Copy link

@isiahmeadows now, at first this issue was about not being required to duck type, then, on my input this was made more universal and rightly so.

And the original was exactly about people neither reading nor remembering API docs and being unsure when to apply @decorator([optionals]) and when to simply use @decorator and to leverage that situation by making both @decorator([optionals]) and @decorator the same.

Hence this proposal and the proposal found in #62. In either case, the proposal found in #62 is more sound. And not just because I made it, but simply because it requires less effort by both the authors of decorators, regarding one having to additionally define decorator[Symbol.decoratorCall] with this proposal and the implementors of both engines and transpilers having to cope with that extra indirection. In addition, the proposal in #62 also will eliminate said duck typing.

And not considering my introducing statement in there, that I am happy with the situation as it currently is.

@Download
Copy link
Author

@silkentrance

introducing a new Symbol.decoratorCall is not very different and requires both the engine and the transpilers to be aware of such a built-in Symbol.decoratorCall.

True. This is why I originally proposed just allowing client code to override call on the decorator function. But as this is a stage 1 proposal, and ecmascript seems to have made up it's mind about Symbol, I am now with @isiahmeadows that it makes more sense to use that mechanism going forward.

leaving us again with the duck typing process

Again true. And again I'm with Isiah on this that it's just a much rarer use case and that it's not worth sacrificing the simple elegance of a simple function accepting plain arguments for it. That said, I don't think the two proposals are actually incompatible. Even if #62 were adopted, it would probably still be powerful to allow the decoration process itself to be trapped.

The biggest difference for me is that this proposal is backwards compatible. Which means that adding it will not break any existing code. As I am seeing decorators used in lots of places already (and regularly find myself writing them) I think we need a really good reason to break back compat. We shouldn't just because we can (because this is stage 1). Instead we should only do it if we have a really important reason that cannot be avoided otherwise. That is my opinion.

@silkentrance
Copy link

@Download breaking backwards compatibility with a stage 1 feature is a simple to do thing. just consider the transition from babel 5 to babel 6 and how many formerly working packages had to be adapted in order to make them work again. as such I consider this a non argument.

Also considering

https://github.com/jayphelps/core-decorators.js#future-compatibility

and

https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Decorators.md#introduction

people are already expecting fundamental changes.

@Download
Copy link
Author

But every time it happens people get hurt. Those updates cost time and money. It will make people more wary of adopting new features if it happens too much.

Standards creation is a delicate process. Look what happened with HTML. It just got stuck for almost a decade. What we need is market pressure. There should be so much code out there using decorators that offering them natively will become a selling point. Then, when they become available natively on some platforms, more people start using them, increasing the pressure on other vendors to follow suit. But if there is hardly any code using it, vendors might hold off on implementing them natively, which in turn may cause devs to hold off on using them. There is a real chicken and egg risk. Just because some committee ratifies some standard does not mean it will actually be implemented and used.

I'm just a huge fan of decorators and want them to thrive. I think that will happen faster if we avoid breaking code from early adopters. Nothing kills the joy of using some cutting-edge tech than having it fail on you frequently and being questioned (e.g. by management or even just peers) on why you are using experimental features.

@silkentrance
Copy link

@Download sorry, but I was still working on the post, see #65 (comment) for an addition.

@Download
Copy link
Author

I think we'll have to agree to disagree here :)

@silkentrance
Copy link

@Download OFFTOPIC considering that HTML was stuck in that dilemma was just caused by market pressure. What we need is a future proof and extensible solution. One that even the peops over at core-decorators.js and similar such frameworks, and babel and TypeScript can live with. Not to mention engine developers over at Mozilla, Apple and Google or elsewhere.

And while I still believe that the current proposal for decorators is a very sane and future proof approach, lest the expense of duck typing and reading API docs (see #23), I think that this approach is something that will cause people to refrain from implementing decorators. The same is true for #62, of course, as it adds additional complexity to a decorator implementation. And all of that extra complexity stemming from #23 and the "inability" of oneself to memorize API docs or failing to properly analyze/debug existing code.

@dead-claudia
Copy link

@silkentrance

I would have to disagree about the added cognitive load in this vs your proposal. I've already addressed most of why I disagree yours is conceptually simpler, but here's a gist comparing the two approaches. It appears that in practice, there's not a ton of difference between the two. Yours actually surprisingly requires a little more duck typing in the decorator itself, from what I found.

As for the current state of the decorator proposal, I wouldn't feel bad if nothing is done about it here. I can always send an almost-trivial PR Angular's and Aurelia's way if this/yours end up not panning out.


(It's okay to TL;DR past this part. 😉)

If anything, I did notice that in yours, there's a little bit of boilerplate for yours that basically boils down to something like this (what I used throughout that gist):

export default function decorate(...args) {
  if (args[0] instanceof DecorationDescriptor) {
    return reallyDecorate(args[0])
  } else {
    return dd => reallyDecorate(dd, ...args)
  }
}

function reallyDecorate(dd, ...args) {
  // do things
}

Alternately, you could do something like this:

export default function decorate(dd, ...args) {
  if (!(dd instanceof DecorationDescriptor)) {
    return dd1 => decorate(dd1, dd, ...args)
  }

  // do things
}

Mine effectively ends up something like this:

decorate[Symbol.decoratorCall] = decorate
export default function decorate(...args) {
  return (target, prop, descriptor) => {
    // do things
  }
}

@silkentrance
Copy link

@Download see https://github.com/tc39/proposal-decorators for the new spec proposal.

@Download
Copy link
Author

@silkentrance Thanks for the heads-up!

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