Skip to content

Allow decorators for functions as well #4

@svieira

Description

@svieira

Decorators for classes, methods, and object properties are a really nice extension to the language. The only extra one I would like to see is the ability to add decorators to function declarations:

@RunOnce
function expensiveOperation() {
    return 1 + 1;
}

de-sugars to:

var expensiveOperation = RunOnce(function expensiveOperation() {
  return 1 + 1;
});

This suggests allowing decorators for any assignment, but I think that might be a bit much so I'm leaving it out of this PR:

// For the curious, that would allow constructs like this
@RunOnce
let expensiveOp = () => 1 + 1

// De-sugars to

let expensiveOp = RunOnce(() => 1 + 1);

Activity

sebmck

sebmck commented on Mar 19, 2015

@sebmck

The issue with adding decorators to function declarations is that they're hoisted which means hoisting the decorators too which changes the position in which they're evaluated.

svieira

svieira commented on Mar 19, 2015

@svieira
Author

True, and that is a potential foot-gun, but I cannot think of any cases where source position is likely to be needed at run-time (I can see using decorators as targets for compile-time transformations using sweet-js, but in those cases the function hoisting behavior does not apply.) As I understand it, hoisting is likely to (though not specified to) happen in order, so even if you had a stateful decorator that added each decorated function to a queue, the functions would still run in order.

Am I just being dense and missing an obvious use case where source position could make or break functionality?

sebmck

sebmck commented on Mar 19, 2015

@sebmck

Some examples:

var counter = 0;

var add = function () {
  // this function isn't even accessible if the decorator evaluation is hoisted
  // ie. done before `var counter = 0;`
  counter++;
};

@add
function foo() {

}

// what is the value of `counter`?
// if it's hoisted then it would be 2 when logically it should be 1

@add
function foo() {

}
var readOnly = require("some-decorator");

// is the evaluation hoisted?
// if so `readOnly` wont be defined if not then any reference to `foo` before the
// decorator will get the bare function
@readOnly
function foo() {

}

The behaviour is non-obvious and it's impossible to sprinkle to get obvious behaviour. It's similar to class declarations not being hoisted because their extends clause can be an arbitrary expression.

svieira

svieira commented on Mar 19, 2015

@svieira
Author

Chuckles Fair enough - thanks for taking the time to point out the issues!

nmn

nmn commented on Apr 9, 2015

@nmn

The noted problems here suggest to me that decorators are possibly going to have very much the same problem as operator overloading is supposed to have.

The major concern I have with decorators is people writing too many decorators with not very good names. Or even for things that are pretty complicated. The concern I have is that decorators are only syntactic sugar to make code more declarative. But I hope they won't end up creating code that is often misleading and full of surprises.

I personally don't think any feature's merits should be judged by considering the worse use-cases. But I think it's still interesting to think about them to remove as many footguns as possible.


Relevant Point: Perhaps, functions decorators SHOULD work for functions. But since hoisting is the problem, Hoisting could be disabled for any function that is decorated. Since Function assignments work the same way, I don't think its too confusing.

sebmck

sebmck commented on Apr 9, 2015

@sebmck

@nmn

But since hoisting is the problem, Hoisting could be disabled for any function that is decorated. Since Function assignments work the same way, I don't think its too confusing.

It is confusing. Function declarations that are hoisted sometimes isn't going to cut it.

nmn

nmn commented on Apr 9, 2015

@nmn

@sebmck Fair point.

Thinking about it more, perhaps they are no so important for functions.

Functions that take take and return functions are pretty easy to write. They are more useful in classes as the syntax keeps things from getting hairy.

Thanks.

Ciantic

Ciantic commented on Apr 17, 2015

@Ciantic

Why are they not important for functions? Memoize is just as needed in functions as well as in classes. Also for currying, who doesn't curry their functions ;)

If problem is you can accidentally use class decorators in functions, maybe another syntax:

@@memoize
function something() {
}

double at, or something.

Edit: Is hoisting really a problem? Being used Python a lot which implements function decorator, I don't think the order of decoration is used to anything except in bad code, which is really hard to safeguard anyway.

sebmck

sebmck commented on Apr 17, 2015

@sebmck

@spleen387 Hoisting is still a problem.

nmn

nmn commented on Apr 17, 2015

@nmn

first a question: do generator functions get hoisted? If not isn't that confusing as well.

If yes, there is already a proposal for making custom wrappers around generators, like async functions.

async function x(){}
// becomes
function x(){
  return async(function*(){
    ...
  })()
}

How does this proposal deal with hoisting?

nmn

nmn commented on Apr 17, 2015

@nmn

Another approach, let the function get hoisted. But the decorator should be applied where it is defined.

So this:

var readOnly = require("some-decorator");

@readOnly
function foo() {

}

will desugar (and hoist) to:

function foo() {
}
var readOnly = require("some-decorator");

foo = readOnly(foo)
sebmck

sebmck commented on Apr 17, 2015

@sebmck

Generator function declarations are definently hoisted. Hoisting the
function declaration and not the decorator is unintuitive and confusing.
You can just wrap the function in a method call and it expresses the exact
same thing without the confusing semantics.

On Friday, 17 April 2015, Naman Goel notifications@github.com wrote:

Another approach, let the function get hoisted. But the decorator should
be applied where it is defined.

So this:

var readOnly = require("some-decorator");

@readonly
function foo() {

}

will desugar (and hoist) to:

function foo() {
}
var readOnly = require("some-decorator");

foo = readOnly(foo)

Reply to this email directly or view it on GitHub
#4 (comment)
.

Sebastian McKenzie

nmn

nmn commented on Apr 17, 2015

@nmn

@sebmck I just updated my first comment. How does compositional functions deal with hoisting?

You can just wrap the function in a method call and it expresses the exact same thing without the confusing semantics.
I get that, but the same could be said about classes as well.

I don't like the idea of decorators for only classes, and I'm just trying to find a solution to the hoisting problem. I feel like having decorators for only classes makes the language more disjointed and inconsistent than it already is.

51 remaining items

ackvf

ackvf commented on Mar 13, 2018

@ackvf

const and let behave slightly different. Cannot we have decorators for these?

@foo
const fun = () => {}
ukari

ukari commented on Mar 13, 2018

@ukari

@ackvf
identifier claimed by const can't be reassign, so the snippet you give can't be translate into reassign-style

const fun = () => {}
fun = foo(fun)

instead of reassign-style, it could be translate into wrap-style

const fun = foo(() => {})

but reassgin-style has a benefit that help foo get identifier's name when it assign to a function, like this

let foo = fn => (console.log(fn.name), fn)
let fun = ()  => {}
fun = foo(fun)

I implement let decorator in babel, here is a example which needs the feature to get let function's name.

javascript-let-decorators

Hypnosphi

Hypnosphi commented on May 4, 2018

@Hypnosphi

You might want to use pipeline operator for that instead:

const foo = x => x * x
  |> memoize
  |> debounce(100)
stylemistake

stylemistake commented on Jul 2, 2018

@stylemistake

Why do we have a problem with removing hoisting altogether? Classes don't hoist, const/let declarations don't hoist as well. I suggest that at the moment user decorates a function, it becomes unhoisted, and it's his responsibility to properly position that function within the code, just like with classes.

For example this:

@decorate
function foo() { return 1; }
console.log(foo()); // 1

...gets transformed into this:

const foo = decorate(function foo() { return 1; });
console.log(foo()); // 1

And if user assumes that it's hoisted, throw a ReferenceError:

console.log(foo()); // 1 - OK
function foo() { return 1; }
console.log(foo()); // 1
console.log(foo()); // ReferenceError
@decorate
function foo() { return 1; }
console.log(foo());

I believe JS should softly guide people away from hoisting, and remove bad language features overall.

mlanza

mlanza commented on Jul 5, 2018

@mlanza

It'd be a shame to exclude decorators from plain functions.

It was said that hoisting isn't going away, one can assume backwards compatibility. But what if something akin to "use strict" – e.g. "no hoisting" – could be added to the top of a script/module? It would be considered a compilation hint. You would only use it in situations like this where hoisting gets in the way.

Macil

Macil commented on Jul 5, 2018

@Macil

My understanding is that the big reason a script-wide feature like strict mode actually made it through in the first place was not because it threw away old misfeatures, but because the act of throwing away those misfeatures enabled brand new valuable use cases (SES / Google Caja javascript sandboxing). And the separate modes idea finally went away in the new happy-path case: modules force strict mode on. I think the standards groups would be extremely hesitant to reverse that victory to re-add a new script/module-wide "use stricter" mode.

Instead of script/module-wide settings, I think it makes a lot of sense to look at how the async-await feature added a new "await" keyword. "await" was not a reserved word in Javascript prior to async-await being added, so if it were added as a new reserved word, then all code that used "await" as a variable name would break. The standard solved this by making "await" a keyword only inside of async functions. The standard avoided breaking old code by making you opt in to the new behavior by making a function async.

Making it so decorated functions don't hoist seems to me like a strategy consistent with how async-await was added without breaking old code. You locally opt in to the new behavior by using a new feature.

tlrobinson

tlrobinson commented on Mar 7, 2019

@tlrobinson

I don't like being that +1 guy, but this is sorely missed. Assignment decorators (tc39/proposal-decorators#51) and disabling hoisting on decorated function declarations seem like very reasonable solutions.

dimaqq

dimaqq commented on May 30, 2019

@dimaqq

Come from the fair land of Python, [plain / const x = () =>] function decorators are perhaps more important than class member function decorators.

finom

finom commented on Aug 29, 2019

@finom

Decorators for functions allow to define a custom how a function should behave. We already have similar and a "hardcoded" syntax of async functions:

async function foo() {}

(yep, there is also await syntax but it doesn't matter at this case)
Why not to allow to do that for custom function modifications:

@something function foo() {}

A more specific example (the thing I was trying to implement before I discovered that there is no decorators for functions) is a multi-thread calculation via inline Worker:

@worker function myWorker() {}
await myWorker();

And currently I need to wrap the myWorker function like this:

const myWorker = worker(function myWorker() {});
await myWorker();

or worse:

const myWorker = function myWorker() {} |> worker;
await myWorker();

which is very inobvious if a function gets more than a few lines.

This is a really wanted feature. I hope somebody more experienced than me could make a spec for that.

More examples (also borrowed from above messages):

// call the function on window load and store it for further use
@onload function onWindowLoad() {}
// call the original function only after something is happened (like window load)
@onsomethinghappen function() {
  // do something
}

// currently it can be done using async function and an additional await call
async function() {
  await waitForSomethingHappen;
  // do something
}
// a debounced or throttled function
@debounce(100) function debounced() {}
@throttle(100) function throttled() {}
// log some information on function call
@log function logged() {}
// a function which doesn't throw an error even if it's happened
@try function mayThrowAnError() {}
// a momoized functional React component
const ReactComponent = @memo () => (<div>Hello world</div>)
// delayed function
@timeout(1000) function delayed() {}

Some syntax examples:

@decorator function() {}
@decorator(...args) function() {}
const foo = @decorator function() {}
const foo = @decorator () => {}
return @decorator () => {}
export default @decorator async () => {}
finom

finom commented on Sep 4, 2019

@finom

Guys, I've made a little article about the function decorators: https://github.com/finom/function-decorators-proposal and also started a discussion at ESDiscuss mailing list. I don't know where it leads but I hope to get at least a little chance of approval of stage 0 from TC39 committee.

TL;DR

And this is the most important. I propose to make a decorated function declaration behave as let variable definition.

@decorator1 @decorator2 function foo() { return "Hello" }

// Will become:

let foo = decorate([decorator1, decorator2], function foo() { return "Hello" }); // no hoisting!
JulianLang

JulianLang commented on Feb 10, 2020

@JulianLang

How are you getting that decorators aren’t a good idea just because you can’t put them on function declarations because of hoisting? An entire feature shouldn’t be nerfed because of one nefarious case.

I imagine how people begin to write single method classes only to get decoration feature enabled where basic function could satisfy... While decorator functions can be applied without decorator syntax
that's noisy and inconsistent.

@sebmck you're the best of us aware of ES(any). Any change to kill this hoisting behavior ever? That is hoisting that allows to use undefined vars hiding syntax errors until runtime. Right? Probably the worst JS language aspect 👿

Actually this reality today, as this is what one have to do in Angular for Pipes:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent?: number): number {
    return Math.pow(value, isNaN(exponent) ? 1 : exponent);
  }
}

see: https://angular.io/guide/pipes#custom-pipes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @tlrobinson@rtm@mlanza@Ciantic@loganfsmyth

        Issue actions

          Allow decorators for functions as well · Issue #4 · wycats/javascript-decorators