Closed Alphare closed 6 years ago
One complication with function declarations is that they are hoisted - and they might be hoisted prior to where myDecorator
is available or declared.
I guess so, but I would expect programmers to be aware of this limitation and arrange their code accordingly.
The only way to arrange your code such that it would work is if you had function myDecorator() {}
declared before your decorated function declaration, or if you import
ed myDecorator
from somewhere first - that would ace out a lot of coding styles.
The requirement to have decorators declared before their use on functions seems not very constraining - non-hoisted functions like Python's have the same requirement. And most decorators will probably come from third-party libraries.
I feel function decorators are even more wanted than class decorators, as people are quite used to them in other languages.
Most points I would make regarding the syntax, use cases and motivation are contained within the PEP 318 document, should that be useful.
EDIT This bit is funny, we're in the exact opposite situation here:
Modifying classes in this fashion is also possible, though the benefits are not as immediately apparent. Almost certainly, anything which could be done with class decorators could be done using metaclasses, but using metaclasses is sufficiently obscure that there is some attraction to having an easier way to make simple modifications to classes.
As @ljharb has said here, function hoisting is the big thing which makes supporting decorated function declarations in JavaScript different, and the difference with Python is that Python doesn't have function hoisting in general. I worry that if we make the way functions are bound different when they are decorated, the effect will be confusion about how things don't quite match up. For that reason, this proposal just adds class, method and field decorators and not function decorators.
But what about const foo = () =>
functions? They are not hoisted right?
@iddan No, they are not, but is that a common enough idiom to warrant a language feature around? And why not just do const foo = decorator(() => ...);
as you can do today already?
Because that's the whole point of decorators - syntax sugar and clear meta-programming. You can use the same case for classes as well - why not just do const foo = decorator(class {})
.
An example with recompose:
@pure
@withState('data', 'setData', null)
const MyComponent = ({ data, setData }) => (
<div onClick={() => setData(Math.random())}>{data}</div>
);
const MyComponent = pure(withState('data', 'setData', null)(({ data, setData }) => (
<div onClick={() => setData(Math.random())}>{data}</div>
));
or more precisely to retain function name
const MyComponent = pure(withState('data', 'setData', null)(function MyComponent ({ data, setData }) {
return <div onClick={() => setData(Math.random())}>{data}</div>
}));
The first example is more readable. The visual pattern of const [NAME] = (ARGS) => RESULT
stays and you get each higher order component to be on another line.
When composing functions as long as the |>
operator doesn't exists composing functions and applying them on inline functions becomes cumbersome and unreadable.
In the second example which is the common case for inlining functions and higher order ones MyComponent.name
is "MyComponent" and not "" - this is very important for debugging and making sense of code.
Also Meta programming can be easily distinguished visually for example in:
@cached
const getData = () => localStorage.getItem('data')
getData()
definition is easily distinguishable from the @cached
which only suggests adding external functionality
@littledan Wrong close, should be tc39/proposal-decorators-previous#40
rather than tc39/proposal-decorators#40
.
@littledan Could you re-open this issue?
I'd still like to leave this feature for a follow-on proposal, due to both the issues described above and the lesser utility of it compared to class decorators (although use cases are demonstrated above). I'd encourage people who are interested in the feature to make a separate proposal repository and prototype various implementations in transpilers, to get more of data on the tradeoffs and motivation here.
Previous discussion: https://github.com/tc39/proposal-decorators-previous/issues/35
Two questions:
Parameter decorators are at Stage 1
@littledan From tc39/proposals it's still stage 0, isn't it?
The best solution for the hoisting problem I can imagine is transform
@deco function f() {
funcBody
}
to
function f(...args) {
$do_decorate_f()
return $decorated_f.call(this, ...args)
}
var $decorated_f
function $do_decorate_f() {
if (!$decorated_f) $decorated_f = deco(function f() {
funcBody
})
}
$do_decorate_f()
Aka, not hoist the execution of decorator until first call.
@trotyl Oh, you're right, thanks.
@iddan @Alphare @hax Would you be interested in starting another repository to continue this investigation? Please let me know if the instructions are unclear or if I could help with anything.
Yes, I'll start a repo.
@hax, your example only works if the decorator deals with the function's execution, but not if the decorator augments the function object itself.
My opinion is that decorating a function declaration should transform it into something like this:
var f = @deco function() {
funcBody
}
However that means adding a decorator to a function might mean needing to move the entire function around in your code if you were depending on function declaration hoisting.
Another possibility is to introduce block-scoped function declaration syntax like this:
let function f() {}
const function g() {}
And then only permitting decorators on a block-scoped function declaration. It still requires moving the function, but it is visually consistent with block-scoped variable declarations.
A third option would be to make decorator evaluation lazy and apply it at one of two times (whichever comes first):
The closest approximation of this approach in existing code would be this:
let g = f; // access f before declaration
f(); // invoke f before declaration
@deco function f() {
f(); // invoke f inside of declaration
}
Becomes:
let g = $$f_accessor(); // access f before declaration
(void 0, $$f_accessor())(); // invoke f inside of declaration
function f() {
f(); // invoke f inside of declaration
}
var $$f_decorated;
function $$f_accessor() {
return $$f_decorated || ($$f_decorated = f = deco(f));
}
$$f_accessor(); // eval decorator if not already accessed
I like the third option. With a little tree cleaning this will work in any situation and usually will only require using the naïve implementation.
@rbuckton The third option won't work with circular dependency in ES module.
Given the scenario:
// main.mjs
import './f';
// f.mjs
import { g } from './g';
export function f ( x ) {
return x + 1;
}
console.log(g(1));
// g.mjs
import { f } from './f';
export function g(x) {
return x - 1;
}
console.log(f(1));
Which should output 2
and 0
;
When decorating f
with some decorator:
// f.mjs
import { g } from './g';
function deco(fn) {
return function (x) {
return fn(x) + 100;
}
}
export @deco function f ( x ) {
return x + 1;
}
console.log(g(1));
Option a): still exports f
itself:
// ...
export function f ( x ) {
return x + 1;
}
var $$f_decorated;
function $$f_accessor() {
return $$f_decorated || ($$f_decorated = f = deco(f));
}
$$f_accessor();
console.log(g(1));
Then undecorated original function gets called.
Option b): exports the decorated one:
// ...
function f ( x ) {
return x + 1;
}
var $$f_decorated;
export { $$f_decorated as f }
function $$f_accessor() {
return $$f_decorated || ($$f_decorated = f = deco(f));
}
$$f_accessor();
console.log(g(1));
Then it would throw due to calling undefined
: TypeError: f is not a function
.
Option c): exports the accessor call result:
// ...
function f ( x ) {
return x + 1;
}
var $$f_decorated;
function $$f_accessor() {
return $$f_decorated || ($$f_decorated = f = deco(f));
}
var $$f_accessor_export = $$f_accessor();
export { $$f_accessor_export as f }
console.log(g(1));
Also got undefined and throws as above.
So there's still no way to keep the behavior consistent between decorated and undecorated functions. The result becomes:
The decorated function would be hoisted in most cases, but not hoisted in some special cases
That's a pretty bad idea for consistency.
Also, making decorator affects execution of dependent files would break current module semantics, and making transformer depends on external files could be very complex (if possible) and cannot help with libraries.
One solution could be waiting for https://github.com/tc39/proposal-module-get lands first.
Not possible even with that landed.
How would it be if we closed this thread and continued discussion on @iddan 's new repo?
@littledan Fine with me, thanks. :)
@trotyl @rbuckton I copy the recent related comments to https://github.com/iddan/proposal-function-expression-decorators/issues/3 , @littledan you can close or lock this thread, thank you.
Hi everyone, I'm not sure to understand the hoisting problem, since the decoration is performed at runtime. The decorator is not evaluated until it's used :
try {
test();
}
catch (e) {
console.log(e.message); // "TypeError: decorator is not a function"
}
@decorator
function test() {
console.log('test');
}
var decorator = function() {
console.log('decorator');
}
test();
// decorator
// test
This seems to me to be the expected behavior : the decorator
method is called only when used (ie when calling the test
function). Hoisted before or after the decorated function or not at all is not a problem, since it will only be used at runtime.
We have exactly the same probem with classes:
var namespace = {};
@namespace.decorator
class Test {
}
new Test(); "TypeError: namespace.decorator is not a function"
var namespace = {
decorator : function() {}
}
@AbrahamTewa As @hax said above, let's follow up in https://github.com/iddan/proposal-function-expression-decorators/issues/3 . Closing this thread per @hax's suggestion to avoid confusion.
Is there any update on function decorators? EXTENSIONS.md
mentions function decorators but there doesn't seem be any proposal?
TypeScript is implementing decorators, so this may be the right time to propose something for function decorators.
Function decorators would be a separate proposal and would need to advance through the stage process from the beginning on their own, so they do not have much of impact on TS implementing the current decorators proposal.
Hi,
At the bottom of the README, one can read "Arguments and function declarations cannot be decorated.". Is there a reason why functions should not be decorated? Coming from a Python development background, my (only) use-case for decorators is to wrap functions, allowing me to extract redundant logic, short-circuit and do other great meta-programming things.
I would expect to be able to write code like:
and
Edit Maybe a better way of writing lambdas with decorators:
Argument decoration seems like a great feature as well:
Thank you!