-
Notifications
You must be signed in to change notification settings - Fork 127
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
class decorator optional opts #23
Comments
I believe this is valid syntax: @dec()
class AnotherClass{
// ...
} That should solve your use case. |
Yeah, I know but it feels weird. |
True. I was thinking that as I typed that reply. And as for the checking, I On Sat, Jun 20, 2015, 06:55 Kamil Tomšík [email protected] wrote:
|
i have been struggling with the same thing. I wanted an easy way to create decorators that could be used with or without the parenthesis. |
If you want a proper, complete checker, you'd have to write a helper function for it, and it could still be theoretically fooled if it's given something that looks enough like a duck. |
Although in practice, it is not quite that likely. |
Is there any reason to not require the outer function to always return a decorator function as the OP suggests? It would make things far simpler. |
@lukescott yeah, that was my point - but I'm afraid it's too late as there is a lot of libs already using it. |
@lukescott @cztomsik I think always wrapping in a function makes sense, to avoid confusion. Existing libs shouldn't be hard to fix? |
Shouldn't be, as far as I know. On Wed, Nov 4, 2015, 05:42 Jeff Hansen [email protected] wrote:
|
I have 2 production app's that are heavily powered by custom decorators, so I know for a fact that it won't be difficult. |
@cztomsik Either they didn't think of this, or they don't trust this: function InjectableImpl(target, prop, desc, opts = {}) {
// ...
}
function Injectable(opts, prop, desc) {
// You can check `opts` and/or `prop` as well, if you want extra insurance.
if (arguments.length === 3 && typeof desc === "object") {
return InjectableImpl(opts, prop, desc)
} else {
return (target, prop, desc) => InjectableImpl(target, prop, desc, opts)
}
} It's possible to overload a function and just check the types to make sure it's not being used as/like a decorator. Should this be able to fix the other half of the issue? |
That's also possible in TypeScript as well. You just need to overload the function type signature. function InjectableImpl(
target: any,
prop: string,
desc: PropertyDescriptor,
opts: Options = {}
) {
// ...
}
interface InjectableDecorator {
(target: any, prop: string, desc: PropertyDescriptor): any;
}
function Injectable(opts: Options): InjectableDecorator;
function Injectable(opts: any, prop: string, desc: PropertyDescriptor): any;
function Injectable(opts: any, prop: string, desc: PropertyDescriptor) {
// You can check `opts` and/or `prop` as well, if you want extra insurance.
if (arguments.length === 3 && typeof desc === "object") {
return InjectableImpl(opts, prop, desc)
} else {
return (target: any, prop: string, desc: PropertyDescriptor) => {
InjectableImpl(target, prop, desc, <Options> opts)
}
}
} |
So, it should be possible to optionally take arguments, if you're willing to accept a little bit of boilerplate. For a general solution assuming a single options object is taken: function makeDecorator(callback) {
return function (opts, _, desc) {
// You can check `opts` and/or `prop` as well, if you want extra
// insurance.
if (arguments.length === 3 && typeof desc === "object") {
return callback({}).apply(this, arguments)
} else {
return callback(opts !== undefined ? opts : {})
}
}
}
// Example usage:
var Injectable = makeDecorator(function (opts) {
return function (target, prop, desc) {
// do stuff
}
}) Or if you'd prefer just a single function: function makeDecorator(callback) {
return function (opts, prop, desc) {
// You can check `opts` and/or `prop` as well, if you want extra insurance.
if (arguments.length === 3 && typeof desc === "object") {
return callback({}, opts, prop, desc)
} else {
if (opts === undefined) opts = {}
return function (target, prop, desc) {
return callback(opts, target, prop, desc)
}
}
}
}
// Example usage:
var Injectable = makeDecorator(function (opts, target, prop, desc) {
// do stuff
}) |
Why do we have to do this in the first place?
It's cryptic! |
@cztomsik All I did was check the arguments and a simple sanity check to ensure the last argument at least somewhat looks like a descriptor. It's just an overloaded function, where it either applies the decorator with no options, or binds the options and returns a decorator. Nothing cryptic there, unless you're referencing the ES6 arrow function. It's just 5 lines of code. And like I also commented, there's still a way to conceal all that behind a function. And I ended up using another similar technique to ensure both of these worked as anticipated: var f = Param(Foo)(function (foo) {
return foo + 1
})
class Foo {
@Return(Types.Number)
bar() { return 1 }
} Believe it or not, that overload is slightly more complicated than this. |
Sorry, my point was that spec itself leads to cryptic code - in its current state it's hard to explain to coworkers. If there was no if/else, it would have been easier. What's an actual usage of paramless version - in which case it is better than regular And if there is any - is it really worth of bugs it may cause - or extra safety code you will have to write? |
And in general, most decorator-based libraries and frameworks coming out now either always require a call like Angular's |
Yeah, and that's why they have such big alert in docs ;-) |
Sorry for chiming in here but this issue ranks high in Google on my searches for information about using braces or not with decorators... I wrote a small test case, but the results are not what I am expecting. From reading this issue I understand this may actually be correct though: describe('decorator', ()=>{
function myDecorator() {
return (target) => {target.decorated = true; return target;}
}
it('works when called with braces', ()=>{
@myDecorator()
class Test {}
expect(Test.decorated).to.equal(true);
})
it('works when called without braces', ()=>{
@myDecorator
class Test {}
expect(Test.decorated).to.equal(true);
})
}); Output:
I would have expected the same results in both cases... Can anyone here maybe refer me to the section in the spec that describes this? And to add my 2 cents... I find the current behavior very unexpected so maybe other devs will feel the same... It may be too confusing for people. I think it would be best if decorators worked the same way no matter what they are decorating,,, |
@Download My workaround is admittedly a hack, but it's a conceptually simple one that doesn't really require much code. |
@Download Yes, it's totally weird |
@Download Also, your example isn't correct. And it shouldn't work. describe('decorator', () => {
function myDecorator() {
return (target) => {target.decorated = true; return target;}
}
it('works when called with braces', () => {
@myDecorator()
class Test {}
// Test is a class.
expect(Reflect.isConstructor(Test)).to.equal(true);
expect(Reflect.isCallable(Test)).to.equal(false);
expect(Test.decorated).to.equal(true);
})
it('works when called without braces', () => {
@myDecorator
class Test {}
// Test is an arrow function.
expect(Reflect.isConstructor(Test)).to.equal(false);
expect(Reflect.isCallable(Test)).to.equal(true);
expect(Test).to.not.have.property("decorated");
})
}); |
Close, invalid/won't fix. |
@silkentrance I hear reasonable concerns voiced here on a proposal that is still WIP...This issue ranks high in Google when searching for info related to this... So please don't close just yet... At least these concerns should be adressed in the specs somehow. |
@isiahmeadows Your response is more or less describing how it works now, however I was describing how I would expect it to work. Based on my experience with other languages (notably Java which has annotations with a very similar syntax) and on 'common sense', I would expect I am probably missing something very big, but at this moment I see no advantages to not doing it this way... only a lot of confusion... But there probably is a very good reason not to do it this way... I just don't see it. |
My thing was that I feel that workaround should be sufficient for most use On Tue, Mar 1, 2016, 09:26 Stijn de Witt [email protected] wrote:
|
Complicate the language? Checking arguments.length is complex. Expecting Why is the decorator syntax like this? function myDecorator() {
if (arguments.length === 3) {
let [target, name, descriptor] = arguments;
return actualMyDecorator(target, name, descriptor);
} else {
return actualMyDecorator;
}
}
function actualMyDecorator(target, name, descriptor) {
// do stuff
return descriptor;
}
@myDecorator method() {} //-> myDecorator(prototype, "method", methodDesc)
@myDecorator("foo") method() {} //-> myDecorator("foo")(prototype, "method", methodDesc) When it could be this? function myDecorator(target, name, descriptor, ...args) {
// do stuff
return descriptor;
}
@myDecorator method() {} //-> myDecorator(prototype, "method", methodDesc)
@myDecorator("foo") method() {} //-> myDecorator(prototype, "method", methodDesc, "foo") Checking length and types isn't fool proof either. What if you expected 3 arguments? What if the first argument was an object? Seems very fragile to me. And to add to this, what if a decorator is used on a method or a class? How do you know which one? More type checking? I added issue #40 for that. The general idea is you do something like this: var myDecorator = Object.createDecorator({
classMethod(target, name, descriptor, ...args) {
// do stuff
return descriptor;
}
}); The above would only support myDecorator on a class method. If you wanted to also use it on a class: // Alternative: new Decorator({...})
var myDecorator = Object.createDecorator({
class(Class, ...args) {
// do stuff
return Class;
},
classMethod(target, name, descriptor, ...args) {
// do stuff
return descriptor;
}
}); Notice how each function signature matches the context of where the decorator is being used. Also, having something like |
@alexbyk and @silkentrance I think your disagreement is based on one fundamental difference: alexbyk really wants decorators to work with or without parentheses, while silkentrance simply does not see any need for that. The point is this: alexbyk is not alone. We've seen many others mention a desire to be able to create 'universal' decorators. And it has been sufficiently proven that this is a) complex to do and b) sometimes even impossible. So maybe we should focus on finding a way to make creating 'universal' decorators easy/possible? Preferably while maintaining backwards compatibility with existing code? I wonder whether it would be possible to abuse the arguments object to be able to tell the difference? function universalDecorator(target) {
if (arguments.decoratorInvocation) {
// decorate target with default options
return target;
}
else {
const someOption = target;
return target => {
// decorate target with someOption;
return target;
}
}
} It would still be work to create 'universal' decorators, but at least the test would be simple and robust. |
You could bind something to decorator.call(target, prop, descriptor)
// vs
decorator('arg1', 'arg2').call(target, prop, decorator) |
Is there a reason why you couldn't just always require a closure, and implicitly convert /cc @wycats (your opinion is worth way more than anyone else's here) Yes, I know I've come a full 180 on that idea, if you've been paying any sort of attention. I'm staring very intently at several libraries/frameworks, including those I mentioned, which make significant changes dangerous. Also, this proposal was pretty stable until a couple months ago, when this bug blew up. |
@isiahmeadows Take a look at Jay Phelps example:
The simple answer is you can't. With Angular and fiends can make both work with the existing grammar, if the decorator function is simplified to the point where it is easy to know whether it is being called as a factory or decorator. Binding |
@alexbyk never mind. the code works as expected. however, and I agree with @Download that our differences are in the way decorators are realized and used and not their implementation on the user side. @Download the problem with passing in a E.g.
Not that I think that this would a wise thing to do, but it is possible. |
@silkentrance What about the use of |
Are you all aware this could have big implications for a massive number of I'd rather not see this bug closed simply from ignoring that fact. On Sat, Mar 12, 2016, 17:06 Carsten Klein [email protected] wrote:
|
@isiahmeadows Yes I'm well aware. As they should be aware that this is a relatively early proposal, which may be subject to change. But if breaking changes can be avoided while still improving the situation, that would be a worthy pursuit no? So... might it be possible to augment the spec in such a way that the scenario discussed in this issue, 'universal decorators' so to speak, would be easy to achieve without fragile duck-typing? I think it would satisfy most of the participants here if that were the case. Hence my proposal to use some kind of non-obtrusive flag to indicate to the function being called whether it is being called 'manually', or by the runtime as part of the decoration process. It might be possible to place such a flag on the All we really need is a reliable way to do: if (/* some flag indicates we are called as a decorator by the runtime */) {
// do my decorator thing
} else {
// do the manual invocation thing
// then return the actual decorator to be called by the runtime
} |
@isiahmeadows Angular 2 users are using decorators, not making them, correct? So what we are talking about affects the authors of Angular 2, not the users. @silkentrance Bound functions are a good point. Although there are already cases in the language where passing a bound function would yield broken behavior. Anyway, I believe @Download I doubt anything could be added to Jay's suggestion so far is best:
function decorator(...args) {
if (args[0] instanceof D) { // D?
let {target, property, descriptor} = args[0];
// decorator
} else {
// factory
}
} |
@lukescott
I can imagine augmenting |
@Download You would have to wrap the special arguments into a special object. Also, I highly doubt that web workers would be a problem as decorators are used on initialization. It's more than likely the decorator is being imported. Backwards compatibility isn't much of an issue for the following reasons:
|
@lukescott I think that backward compatibility is not such a small problem. Angular devs will survive and adapt, but there will be a lot of code lingering around for a long time that simply does not get updated. But much more importantly, it may end up being very off-putting for early adopters and people interested in using decorators... they may take a wait-and-see position, not willing to risk having to rewrite their code yet again. I would hate for this proposal to lose momentum. I think the Babel folk temporarily removing it has already done a lot of damage. Let's not make that worse. So I think we should only consider breaking backward compatibility if it will bring a very clear benefit that cannot be gotten without breaking back. compat. The way I see it, the clear benefit I am after is being able to write 'universal' decorators without brittle duck typing. For that purpose, I only require some clear sign. This could be manny different things as long as it's clear and robust. Changing the function arguments may bring other benefits, but the cost of breaking back. compat outweighs those imho. But we can get this one huge benefit without breaking backward compat so that is what I am now voting for because I don't think we'll ever get more out of this issue and frankly, I don't need more. |
@Download This is stage 1: Stage 0 - Strawman - Expected Changes: N/A (From https://tc39.github.io/process-document/) People jumped on this at stage 0. We wouldn't even be having a discussion about backwards compatibility if it weren't for compilers like babel. At the stage 1 major changes should be expected. Those who use features at this stage should expect to rewrite their code. I wouldn't consider any feature in-process to be "stable" until stage 3. Somewhat stable at stage 2 if you really want to be generous. With the changes themselves, wrapping up the arguments into a single object doesn't change much for the decorators themselves. They can still get to the target, property, and descriptor. In many cases this is a simple rename. For detecting This really isn't something we need to be agonizing over at this stage. |
@lukescott I understand the definitions of the stages, but I don't believe in them. People are as people are. The way I see it, this proposal received a very enthusiastic welcome because people really want decorators. Bad. So bad in fact that they are willing to base significant parts of the new major version of their very popular and influential framework on it (Angular 2). They jumped on this at stage 0 for a reason. They want this and it being an 'official' proposal for the language was enough for them to embrace it. But then things changed and Babel removed support (temporarily...). Suddenly, having these cool decorators ended up as a problem. Of course you know it can happen with stage 0/1 proposals, but that does not mean you want it to happen. Mostly, you just want this thing to move through the proposal phase as quickly as possible and end up implemented in the major browsers so you can use it and be all vanilla JS! Breaking backwards compatibility now may end up killing that early enthusiasm. I for one don't think universal decorators warrant that, especially because I believe we can have the cake and eat it too. I think that whether BC may be broken or not is a significant constraint so I created a separate issue that strictly disallows it. We can then continue using this issue to explore avenues without considering BC. |
@lukescott If a given API is changed to use different idioms, even if they're similar, it's still breaking to both the API creators (Angular 2 devs) and API consumers (Angular 2 users). Just thought I would clarify. @Download I'm not too worried Babel dropping official support is going to break too many hearts, considering this plugin was made to fix that very problem. As for my opinions on what should/shouldn't be done, I think they're pretty well known at this point. I think I'm going to sit back for now, instead of letting my overly opinionated self get out of control. 😄 |
@Download The purpose of stage 1 is to demo the feature as a "high level api" and to "identify potential challenges". Whether you agree with the process or not, it is set up that way for a reason. I believe that the Angular 2 guys are smart enough to know that the decorator functions could change. The syntax itself, which is used by their users, isn't changing. Much larger things happened with the development of the es6 spec. For example, when class methods changed from enumerable to non-enumerable. Changes are inevitable, especially with proposals. |
@lukescott I like the idea of introducing a new built-in object. Wouldn't it be best to discuss this over here #62? |
Since the discussion here revolves around whether #62 and the proposal in there should be made part of the ECMAScript standard, and also since this discussion may not lead to any conclusion, perhaps we all move to #62 instead. There, @Download, who took the initiative in #61, and I, who took the liberty of producing a proposal from the assorted information found in here and there, lest the bashing and other unrelated stuff, we might come to a conclusion that will hopefully benefit all. And sorry for being somewhat obnoxious, but this needed to be stirred in order to make the discussion productive again. And even @cztomsik might find his original case being taken care of in the presented proposal. |
Encountered the same problem in my projects. Created this helper function: https://github.com/le0nik/decorate-with-options. |
BTW, the current state of the proposal can be seen here. |
To be honest I am a bit disappointed. There has been lots of discussion here on this and other related issues about the duck typing / decorator vs factory / with/without parentheses issues, but I see nothing mentioned on it anywhere in the new proposal. It looks like the whole discussion has been completely ignored... (could be I'm reading it all wrong and it has in fact been addressed or at least mentioned somewhere... if so, please point me to the relevant text because I can't find it) |
@Download the overall discussion was moved. And there is also a new issue there that, unfortunately, tries to revive this one... #75 (comment) |
Is it really necessary to make difference between regular class decorators and those accepting arguments?
Because it would be great if we didn't have to test presence of arguments, how would you implement @dec with optional arguments?
It feels inconsistent - I'd rather have to return function all the time instead of doing nasty
isClass
checksThe text was updated successfully, but these errors were encountered: