-
Notifications
You must be signed in to change notification settings - Fork 16
Decorator call signatures are ambiguous #24
Comments
You can try for the 'options object' option: function foo(opts, ...args) {
if (args.length 0) // function was not used as a class decorator
{cls} = opts;
return C => {...};
else // used as a class decorator
return dec_cls(opts, ..args);
}
@foo
class A {}
@foo({cls: A})
class B {} ... or something like that |
Thanks for the suggestion @MMeent - that is a good workaround if this ambiguity is not resolved before the spec is finalized. I do hope we can change the argument passing along the lines I suggested there... because the current functional zen (to me) is a really an unfortunate way to express the goal here :) |
I find it amazing that we had this issue on the old repo: And now, one year and nine months later, I see everything has been moved to a new repo. The old issues are gone, like they never existed and here, on this new repo, we get the exact same question.... Hell even the issue number is only one off: 24 vs 23. Seems that's about how long it takes for people to catch up to this hole in the spec. Where are all the ideas mentioned in the discussions on the other repo addressed? Was any consideration given to the problems mentioned? I even came up with an idea to address it myself, but never had any response from anyone involved in the spec. This is what I wrote:
Is the issue tracker here just for fun and to show off how open source Ecma is, or will you guys actually respond to community feedback / questions? What is the idea for this issue? Will we attempt to address it? You can just wait for confusion around this... It would be a shame if the spec was progressing and this issue that has been brought up so early and often was never dealt with. |
Could someone provide a concise summary of the issue? Does the current draft, at proposal-unified-class-features, still not address it? |
Really concise summary: @decorator
class SomeClass {} !== @decorator()
class SomeClass People are not happy about this. Please just spend some time reading the feedback. People placed 150 comments in 2 years time. I can't summarize all that. |
Why wouldn't overloading based on "does this look like a class descriptor" work here? |
@littledan that wouldn't work for the OP which passes a class as a decorator parameter |
@dongryphon I don't see the contradiction; a class looks completely different than a class descriptor, as this proposal uses. You can check |
@littledan Do class-level decorators get a descriptor? From the spec it seems like they are
@dongryphon Either way, class-level decorators don't just take a single argument anymore, so that should help disambiguation. The behavior in |
@littledan As specified in the proposal a DeclaratorMemberExpression is expected to evaluate to a "decorator function" ie, a function whose signature is rigidly defined by the specification. This means that when a DeclaratorMemberExpression is a DeclaratorCallExpression, the function that is being called with the provided Arguments isn't actually a "decorator function" (DF) but instead a higher order function that acts as a factory for DFs. Let's call such higher order functions a "Decorator Factory Function" (DFF). It's important to know that DFs and DFFs have completely different signatures and are called at different times in the class definition process. However, the simplistic conceptual model of decorators that is likely to be taught (and which is implicit in some of the example in the specification) completely blur the important distinction between DFs and DFF. Consider the import { deprecate } from 'core-decorators';
class Person {
@deprecate
facepalm() {}
@deprecate('We stopped facepalming')
facepalmHard() {}
@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
facepalmHarder() {}
} Is export function deprecate(...DFFargs) {
function deprecate_DF(elementDescriptor) {
// body of the DF that is called to actually decorate elements.
// note that it has access to the DFFargs that were passed to the DFF that created it
}
if (args.length == 0 or args.length > 1) return deprecate_DF;
//if args.length is one then we don't know whether we are being invoked as a DF or a DFF
//so we need to infer it by duck typing the argument.
const arg = DFFargs[0];
if (arg.kind && arg.property && "isStatic" in arg && arg.key && arg.descriptor) then {
//looks highly likely that we are being invoked as a DF (but in rare cases we could be wrong)
return deprecate_DF(arg);
}
//probably was called as a DFF with one argument.
return deprecate_DF;
} (Note that the duck typing would be easier and more reliable if the specification used a symbol to brand element descriptors.) Any decorator that is going to access optional arguments is going to have to be written something like the above. This seems like a significant burden upon framework developers (or anyone who wants to define decorators). Also, this sort of confusion between the type of a higher order factory function and type of the functions that such a factory produces seems like a significant bad design smell. It would actually be easier and conceptually cleaner if Decorator : |
@loganfsmyth It sounds like you're looking at an outdated version of the spec. In the current draft, there's a single ClassDescriptor given as an argument to the class decorator. Would this permit the overloading that the OP is requesting? |
@loganfsmyth @littledan @allenwb Thanks for all the clarification. Now that the spec has evolved to passing an |
@littledan Ah right, sorry there are so many different specs now that I've kind of lost track of which one is supposed to be the source of truth at this point. |
@dongryphon I've seen type sniffing in existing decorators designed to serve double-duty as directly called functions, so it seems like this is already an OK practice in at least some of the community, for better or worse. If you have other use cases that can't be served this way, I'd love to hear about them. |
@loganfsmyth Sorry about that; I put it on my todo list to clean up the repository situation here. |
Wait, that is not a good justification for a troublesome design. You see it already because, given the current design, it is the only valid way to code a decorator that accepts optional arguments. That doesn't mean it is "ok in practice". It just means that some people have found the way to work around the design issue. Have you look for places it was miscoded or talked to people using that pattern to see how long it took them realize it was necessary? How many decorators are there out there that are just waiting to fail because somebody puts a As was pointed out up thread (tc39/proposal-decorators#24 (comment)) there is a two year old 150+ comment thread (wycats/javascript-decorators#23) about this very issue. Why are the concerns expressed there and in this thread being blown-off in this manner. The purpose of Stage 2 is at least as much about finding and fixing problems in a proposal as it is about pushing the proposal forward to Stage 3. Let's fix this problem. |
See issue #43 for a possible way to eliminate this problem. |
A bit disappointing to basically see the whole discussion we had two years ago be repeated now again.... But at least it seems this time the people working on the spec are actually paying attention.
So true! That's the whole reason the original discussion was created. Over 150 comments were added by community members, with all kinds of different solutions to this problem. But almost everyone involved agreed that it sucked that @deco
class MyClass {} and @deco()
class MyClass {} Don't do the same thing. But still, with duck typing, the problem remains that authors of decorators need to opt-in to making the braces optional. If they don't do the duck typing trickery, then both code samples shown above might still end up doing completely different things... So users of decorators need to check for each and every decorator whether this decorator requires braces or cannot have them or whether it does duck typing so they can be used or not at will. Lots of confusion around a very basic use case. |
I'm sorry, I'm not trying to blow off any concerns, but I won't have time to read the entirety that thread right now. I thought it'd be better for me to engage here than continue to ignore the issue (which I was not aware of until just now). I don't think any open source project contributors can read every single thing everyone says about it. Having 150 comments actually makes things harder, not easier, to get up to date on.
This particular thing has changed. Rather than decorators being passed the whole class, they are passed a descriptor, which is an object and not callable.
This is very true. If the semantics that everyone wants are that it always acts like this, and calls the argument, that's a really easy, small change. I don't really have a problem with it. It's pretty different from allowing overloading between functions passed as arguments and this thing being invoked as a decorator, though (which, as I explained above, changed since the thread happened). I'm more skeptical of the class-based approach described above, for the reasons described in tc39/proposal-decorators#43 (comment) . I think we could do just the () part without the part of treating it entirely as classes. What would you think of that approach, @Download ? |
I think I revitalised this issue with a comment that I subsequently deleted, once I realised that the spec had advanced since my last read - and because I naively assumed that the recently released Babel transform babel-plugin-transform-decorators exposed the latest Stage 2 approach - which it does not. It is effectively a bugfix and compatibility upgrade to the old legacy transform, but without the legacy suffix - slightly surprising. That comment was motivated by the frustration of attempting to use the legacy decorator approach, again, and being unhappy that I could not consistently enforce The approach in the latest spec certainly improves the situation, previously, one impossible edge case occurred when passing an optional constructor function, to, say, denote a type as a parameter - since there was no way to disambiguate between that, and an invocation of the decorator on a target class. @Validate
class SomeType {
@Validate(String)
method(param) {}
} Being able to duck type by inspecting the shape of the passed descriptor is better, but it still seems like there is a terrible code smell to having two possible interpretations of the same function - that adding or omitting braces changes semantics. That a decorator function can be either (or both), a factory, or (and) an explicit function, seems inherently dangerous - particularly since the onus is on the developer exposing decorators, to decide, and determine which use cases are valid. This will almost certainly lead to a wide mixture of libraries, with different approaches:
It feels like a recipe for a lot of subtle bugs. |
@jka6502 Thanks for writing out these thoughts. After all this discussion, which alternative are you most in favor i? |
@littledan - No problem, I actually like a few of the suggestions, though the most elegant, imho, would be adapting wycats/javascript-decorators#23 (comment) to the new spec, something along the lines of: function decorate(descriptor, ...args) {} Where Alternatively, forcing a factory for every decorator seems viable too - its the mixture of both, and inability to determine which is occurring, that makes it problematic: function decorate(...args) {
return (descriptor) => { /* ... */ };
} I'd imagine some would prefer to stabilise on decorators always representing a factory function, to provide simpler memoization based on arguments - though that could be achieved either way (I actually don't mind either, as long as the parameters of a decorator become consistent, regardless of its method of invocation - i.e., the author knows whether they are writing a factory, or a function): function decorate(descriptor, ...args) {
return someFactory.get(...args)(descriptor);
} It seems like a simple grammar change can accommodate the optional nature of the arguments / braces (tc39/proposal-decorators#43 - @allenwb opened that issue with a simple and clear explanation of the changes needed, and their precedent), too. An interesting note, that I forgot from my previous comment, and that was raised in the previous issue thread, is that there are precedents for both approaches ( Whether you consider that "their own fault" or not, almost analogous existing mechanisms in other languages are going to influence the likelihood of people using decorators correctly (and thus the perception of the usefulness / difficulty of decorators), and, interestingly, those with Python decorators based preconceptions will provide consistent brace / arg usage, which will not fail with equivalent semantics (==), but those with Java Annotations based preconceptions will likely mix and match, which will cause regular bugs with current ambiguous semantics (!=) unless the decorator authors attempt to unify both styles, under the hood. All of the potential solutions suggested affect the signature of decorator functions (or boiler plate class, in one suggestion), meaning that the requirement of deeper understanding lies with those creating decorators, rather than those using them - which sits very well with me (as someone who has encountered this, predominantly, while trying to write decorators). |
Couldn't agree more! If we can get |
As in @jka6502's comment, I would prefer: function decorate(descriptor, arg1, arg2, arg3) {}
// And I could use the spread operator if I need.
function decorate(descriptor, ...myArgs) {} I am not sure the other option is a good alternative: function decorate(...args) {
return (descriptor) => { /* ... */ };
} Correct me if I am wrong but this would create an instance of the function each time the decorator is called which could affect the performance as I am expecting decorators to be used heavily in UIs. |
@borela - On the second approach, perhaps it would be a tad slower, though the enclosed function is still precompiled, rather than entirely unique per invocation of the enclosing function, closures are very well optimised in every engine, since they are a common idiom - they are really just functions with additional 'arg' references defined in the enclosing call site. I personally still prefer the spreadable args approach as well - just pointing out that anything that removes the ambiguity works for me (and the factory function approach clearly has some support - but is achievable manually, regardless of the approach) - I fully intend to use decorators for UI purposes too, but my, admittedly, naive testing has shown that the module load time influence of decoration is pretty negligible, even with heavy use (I've been using https://github.com/jkrems/babel-plugin-transform-decorators-stage-2-initial - though it appears to have died, and is incomplete, unfortunately). |
IMHO:
and some will have properties or fields:
Unless you RTFM you will run into problems if you use it the wrong way at run-time. And in the case of the first type of API the problem will be extremely hard to debug because doing The same holds true for omitting a required argument or passing too many arguments to most APIs out there - many of them will not make your logic fail immediately but all kind of side-effects will crash the application or make it behave unexpectedly somewhere later on. These are the kind of problems devs have to deal with every day in the JS world. And they can only avoid those problems if they know the API and/or have read the docs. So why should this spec jump through hoops in order to improve the situation specifically for decorators, only, sacrificing flexibility and possibly performance at the same time? If API vendors/authors can come up with a consistent API and carefully implement their decorators and properly write their documentation, devs will be able to work with them in no time. Don't get me wrong: I think simplification and removing possible pit falls is a good thing. But not for any price. So keep in mind that many devs are OK with APIs that could be "ambiguous" - that's the situation they are facing today, anyway most of the time: if the APIs are properly documented and implemented, they will be fine with that. APIs which aren't consisted or properly documented or a pain to use will die anyway soon, one would hope. Devs won't be happy, however, if they need to sacrifice performance are even be forced to like always have to append |
@jka6502 We're no longer actually passing a target in. Instead, there's a descriptor passed in, and another descriptor passed out. It sounds like the biggest thing that this thread is asking for is to make |
@littledan that's perfect for me, the rest of the specification seams fine as I only had trouble with optional arguments and detecting whether the class passed in was the target or just an argument. |
For the record: |
They already are treated differently here in that
This invocation is part of the syntactic sugar. So I don't see the analogy as applicable here. The suggestions so far boil down to making the difference between |
I guess the difference depends on whether you see |
I was slightly in favor of "magically added parenthesis" but the example of |
@awesomeDecorator
class SomeClass {
@deprecate
myPropA = 1
@deprecate('custom message')
myPropB = 1
@sub.deprecate
myPropC = 1
}
// vs
@awesomeDecorator()
class SomeClass {
@deprecate()
myPropA = 1
@deprecate('custom message')
myPropB = 1
@sub.deprecate()
myPropC = 1
} It is cleaner without the unnecessary parenthesis. |
I see people writing this:
where it should be
Just because Correct me if I am wrong, but with the current state of the spec it's not impossible. With the updated spec, it can be implemented to work except for the one single very awkward use-case where your "factory" method takes as arguments a descriptor. Only for those decorators where you would want both, no-arg and argument usage, you would have to implement the switch in the code. |
It's not really the same thing to say you are "calling the decorator function" in one case and not the other since it is called immediately in all cases. But this analogy to all other things "function" is really an implementation issue imho. The idea behind decorator syntax is (as I understand it) to feel declarative. Given:
Under the current spec, one would have to write this to process the ambiguity (from here):
A better conversion of the syntax would make all forms easily recognizable:
For decorators that do not care you would simply not have anything beyond |
The good thing about the HOF/factory/currying variant is that you can do costly preprocessing easily and reuse certain "instances" of decorators. Would the following still be possible with the "new", simplified implicit parenthesis approach?
Wouldn't implying the parenthesis make this kind of currying impossible? With the old spec, it was easily possible to implement the above. |
Why would it make it impossible? const hardDeprecation = (descriptor, ...args) => deprecate(descritor, ...args, "STOP IT, STUPID, It's too slow!", fibonacci(4242))
// Or...
const hardDeprecation = (...args) => deprecate(...args, "STOP IT, STUPID, It's too slow!", fibonacci(4242)) |
OK - "impossible" is the wrong word - I can always "decorate a decorator" and implement a new one that calls the former like you have shown. Still with your approach it would be one additional function call for each usage. That may not sound like a lot, but for example the API that I was using decorators (modeled after the old spec) for, there were more than 10K decorator usages. I was hoping to be able to get rid of these extra calls, because they are likely to slow down application startup. I simply don't like the fact that writing decorator annotations in two different ways would be required to result in the same behavior because the parenthesis would be added implicitly if omitted. What would that mean in this case:
would that be different from this
or even
I think that would be really confusing (the part that they would all behave identically). What would you expect to happen here? I am against the "automatic parenthesis insertion" because "refactorings" like above would not be trivial to do and understand anymore. |
There are definitely things to say for both making the with/without parenthesis cases equivalent and not doing so. However, I feel that the use cases described where the current situation is confusing vs those where treating with/without parentheses as equivalent is confusing are clearly the more basic use cases. The examples shown where decorators are properties of other objects, or where those properties are functions creating decorators, are imho much more contrived than say the addHandler(({x, y, z}) => {
// object deconstuction is used to get direct references to members x, y, and z
}) The trend seems to be to get direct references to anything. So given a const { createMegaDecorator } = decoratorFactory
const deco = createMegaDecorator()
@deco
class MyClass {} Of course, this stuff is all personal. But looking at real-world examples, my feeling is that the feature to be able to easily write and use decorators with/without parentheses is more important. |
@Download: I totally agree with you - it should be possible to implement decorators that do not require parenthesis. My point was that this is already possible and can be implemented even without the forced braces behavior that I am opposing. The thread started off that it would be hard for the implementers of the decorator to implement support for both usages. This is not the case. For those decorators that will be used by lots and lots of people, developers will add that feature. But for those cases where you do not want the automatic parenthesis insertion, with the proposed solution this would become impossible. TL;DR; : we can (and will) have @deprecate and @deprecate("foo"), even without the proposed "force-as-call"-constraints. The new proposal would be more restrictive and that's why I would be against it. It will not become difficult to implement decorators that support both variants. As this thread has shown this feature can be added easily on top of the current spec and we don't use the flexibility. |
@yGuy Note how I stressed and use. Sometimes, being more restricitive is a good thing. See also new keywords such as Also I say you can rewrite every instance of decorator use where this would actually matter and the code would end up being simpler and more easy to understand. |
@Download Then again: This new additional restriction will not prevent implementors from creating wrong implementations, either. They will mess up some of the contracts or create buggy decorators, either way. However this restriction will be a restriction for those who could write more sophisticated code or want to get that last tiny bit of performance boost. Why should decorators feel totally different than anything else in the JS world? I don't understand why we are sacrificing flexibility (and adding more magic at the language level!) here just to make it slightly more difficult to shoot yourself in the foot in this specific situation when this is what devs are doing all the day: shooting themselves in the foot because they can! To be honest: I either don't understand your other two paragraphs or I don't see how they should be an argument in favor of the proposed restriction. Last but not least I would like to add that if we add this restriction we are clearly changing it for all typescript users, which are used to doing it like this since day one:
from: https://www.typescriptlang.org/docs/handbook/decorators.html The new spec would have to differ and would result in the more complex wording somewhere along the following lines:
Is this really what you want the spec to look like (not literally, but I hope you see what I mean) ? There is a similar discussion going on in the "dynamic imports" spec. I feel that we have the same issue here: If the decorator looks like a regular function invocation, it should be one. If you want to use a declarative approach that works differently with more magic involved, the whole syntax should be changed instead, so that it doesn't look like an ambiguous invocation anymore. If this is how this spec ends up, I would rather like to see a totally different, unambiguous syntax like e.g.:
That's actually a bit more similar to the way Java and c# do it: Java Annotations use a special syntax for passing arguments to the annotation, which can only used for annotations. So did c# initially, although in newer releases with default and named arguments method calls look quite similar, too, now. None of them allow complex expressions, though, but always require the type, directly. So maybe it's either arbitrary expressions that need to evaluate to a function just like in TypeScript or a very strict syntax that does never allow arbitrary expressions like in Java and c#. And only in the latter case you could allow the "force parenthesis" approach, possibly using a totally different syntax. I would feel that the arbitrary expression without forced invocation/parentesis solution is a lot more JavaScript-like. |
Determinism vs non-determinismThe reason those of us suggesting the In contrast, every counter example you have provided is still possible, but, perhaps, requiring a change in how it is applied. The suggested implementation above (which necessitates I'm not entirely bound to the solution discussed, but I believe the above needs fixed somehow. You list an example in tc39/proposal-decorators#24 (comment): @decoratorFactory.createMegaDecorator()
class Test {}
// same as?
@decoratorFactory.createMegaDecorator
class Test {} Why wouldn't they? What would you expect to happen? Something clearly denoted as a factory, that returns a decorator function, by your own naming, should be capable of being applied as a direct decorator function too? Why would you want that? Do you have an actual use case where differentiating is beneficial? Perhaps I'm just failing to see the wood for the trees here, but all of these examples seem to merely highlight the ambiguity. Also, in tc39/proposal-decorators#24 (comment): import { deprecate } from 'core-decorators';
const hardDeprecation = deprecate("STOP IT, STUPID, It's too slow!", fibonacci(4242));
class Person {
@deprecate
facepalm() {}
@hardDeprecation
facepalmHard() {}
@hardDeprecation
facepalmHardAgain() {}
@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
facepalmHarder() {}
} What if the implementer decided that this should only be a factory function, the The very example you cite aims to unify usage, for exactly the same reasons: Alignment with existing JS conceptsWhere does this argument stop, could additional parameters raise to a second order, should we allow I would argue that this is a new feature, which doesn't precisely align with any existing concept in JavaScript - similar yes, the same, no. If it were, surely we'd wrap the type, or member, in parenthesis, to clarify that this is a function invocation on that element? ( How do multiple decorations of one member compose? Does that align with existing JavaScript semantics (also, hasn't the composition order changed, due to the previous spec applying descriptors between - meaning that class SomeClass {
@deprecate
@readonly
property = 7;
} I do agree that keeping a simple, consistent, approach to as much of the language as possible is very valuable, it is one of the factors that continually draws me to JavaScript - but I don't believe this is being violated in any way here, regardless of approach - as has been pointed out in this discussion, decorators are declarative - this does feel a bit like forcing a square peg through a round hole, because it is "most like the round pegs". Also, neither Java, nor C# have custom calling mechanisms for Annotations - they use parenthesis (and both treat
HOF maybe (though only sometimes), but currying? Would you actually curry that way? At worst, this is premature optimisation, at best, confusing syntactic sugar, for something entirely achievable regardless of the approach taken - decorators can always be the result of currying factories, the argument here is that they shouldn't absolutely have to be currying factories, in every case, too (as well as direct functions). function doSomethingToNumber(message, number) {}
// Would you expect passing something other than a number to cause that function to self-curry, and return a variant of itself? I would raise this as a serious, serious code smell in review, in my place of work.
let a = doSomethingToNumber('Damnit Dave', 7); // TADA - Number returned
let b = doSomethingToNumber('Damnit Dave'); // Should this return doSomethingToNumber(arg), with curried second parameter? Really?
// I've never seen the above in practise anywhere - surely, you'd curry from outwith the function:
function curryMessageParam(message) {
return function(number) { return doSomethingToNumber(message, number); };
}
let doSomethingToNumberSpecialised = curryMessageParam('Damnit Dave');
// But, equally, I wouldn't expect: curryMessageParam('Damnit Dave', 7); to magically directly invoke, either... Prior art
This is a stage 2 proposal, quantity of adoption of an unstable spec, and difficulty in adjusting as the spec moves forward simply cannot influence the end result - that would be madness, even the very link you provided, after the first paragraph, warns of this:
Every proposal could end up tethered to its initial first draft by that rational - Isn't the staging process designed to prevent exactly this? |
I think it is unhelpful to focus the discussion on any of the following snippets around "function calls". This is because the goal of the snippets below is not to make a function calls (the fact that
The goal of the above is rather to decorate a class with appropriate metadata. Because all of this is declarative in nature, I believe the vast majority devs using decorators would reasonably expect the same outcome in the first 2 cases above. This expectation is only called into question (afaict) when devs learn that decorators are implemented as functions. Whatever the above de-sugars into is not really the main point. How many functions and calls are involved here is a distant secondary concern... except as it relates to how implementations would achieve the desired and expected outcome... ideally without even thinking about it. |
Yes! Thank you!
You know, I actually like this idea! But all things considered, I am ok with the current syntax. In fact I'd say I'm ok with the entire spec, except for the way it treats the two cases as completely different when semantically there is no difference. In both cases we have a decorator without any arguments. Let it be the same. |
@dongryphon @Download |
Yes, I agree - but I still think this is a very low prize to pay - can you think of any reasonable use-case where you would want to have a descriptor like argument as the first parameter of a decorator "constructor"? I cannot. If we made the descriptor a proper type that can be checked with |
I guess I am not seeing this de-sugaring as violating any JS concept:
|
Hmm. What would happen here if you'd do this? @decorator.call(thisarg, args)
class foo {} The desugarization would be nontrivial for normal js developers. Yes, it would be bad API design on the side of the decorator-library, but it's a valid use case I guess? On the 'inexplicit parens on decorators', a similar construct exists in JS with the |
I would expect this:
To translate (given the above idea) in to:
Would it work? Depends on what |
Even though the syntax is borrowed from function calls (as it does in most annotation language features) it is not fair to say it is a function call when clearly there must be much more to the consumption of whatever follows the |
Consider:
I am attempting to provide a decorator that accepts fully optional arguments including optional class parameters. Is there some idea brewing how to reliably determine the context of such things?
Given the current spec as I understand it (using
babel-plugin-transform-decorators-legacy
), there is no way to distinguish the above forms. To support case 1 you would do:To support case 2, you would do:
But since the parameter is a class there is no way to tell which form is being invoked.
If I could wish :) I would rather write decorators like so:
The idea there is that
args
is whatever (if anything) is supplied in parens after the decorator name. So to handle both cases:One could debate whether
args
should be an empty array to distinguish@foo
from@foo()
I suppose.The text was updated successfully, but these errors were encountered: