tc39 / proposal-decorators

Decorators for ES6 classes
https://arai-a.github.io/ecma262-compare/?pr=2417
2.76k stars 105 forks source link

Decorators for all definitions #440

Closed samholmes closed 2 years ago

samholmes commented 2 years ago

Currently the decorators proposal specifies decorators which can be applied to classes and class methods/properties. I would like to explore an extension to this proposal whereby one could apply a function to any definition (variable, constant, parameter) using the decorator syntax:

// Constants
const @decorator identifier = value

// Variables
let @decorator identifier
identifier = value

// Function declarations
@decorator function name( parameter) {
}

// Function parameters
function name(@decorator parameter) {
}

Motivation

The motivation to having this extension to the decorator proposal would be cases where you would like to run a function to validate the value assigned. This is particularly useful for runtime type schemes or data normalization.

// Runtime type examples:

// Validate some unknown data from IO
const @PersonType person = getPersonFromIO()

// Implement validation for function parameters
function getPersonName(@PersonType person) {
  // Safely access name property because person has been validated
  return person.name
}

// Data normalization:

const normalizeWhiteSpace = input => {
  if (typeof input !== 'string') throw new TypeError('Expected string as input')
  return input.replace(/\s+/g, ' ')
}

const @normalizeWhiteSpace cleanData = prompt('Enter input')

These examples are not just contrived, but rather are very practical. Allow me to make a case.

Imagine a user-land runtime type system where each type was simply a function which validated input according to that type or threw an TypeError. Such a runtime type system could leverage decorator syntax to annotate the implementation with types and improve the type-safety of a codebase. Furthermore, user-land optimization tooling may be built which may do static analysis of the source code to determine where these runtime types may be safely removed from the transpiled output. This sort of source code not only renders itself as more safe, but more strict by nature; making JavaScript more correct by means of a declarative API (using decorators).

Conclusion

The use-cases that come from allowing decorators to be used in more definition syntaxes are useful. We have an opportunity to add a lot of value to the developer by this extension. However, more discussion is needed to identify shortcomings and potential problematic cases.

samholmes commented 2 years ago

I realize the implementation effort may be significant for transpilers due to the nature of how getter/setters function exclusively on properties of objects. In order for a decorator a variable, constant, or parameter, then perhaps each decorator kind should be "accessor". This way the underlying data structure on decorated variable, constants, parameters is a "top-level auto-accessor". In this sense, what I am proposing is a top-level accessor specification.

Having top-level accessors and allowing them to be decorated with decorators enables a very elegant syntax sugar which not only can we apply a runtime type system as mentioned above, but also functional reactive programming (FRP) patterns:

let @reactive() seconds = 23
let @reactive(num) minutes = parseInt(seconds / 60)

setInterval(() => {
  seconds += 1
}, 1000)

function getTimeString() {
  return `${minutes} minutes and ${seconds} seconds`
}

In this example, a runtime and a function reactive is implemented such that it enables FRP. The reactive function accepts zero or more accessor arguments (tracked accessors) and returns a decorator which is used to decorate an accessor with reactivity. The decorator returns a new accessor with properties:

What this means to illustrate is not valid use-case that is enabled by extending the definition of the decorator proposal. Hopefully this strengthens the argument for such an extension.

Transpilers

It is conceivable that transpilers could implement this syntactic and semantic change to JS by transpiling any occurrence of an identifier which has been semantically changed from a "regular variable/constant" to an "accessor variable/constant" by replacement:

let @decorator foo = 23
console.log(foo)
foo = 42

// becomes
var foo = _decorator(_initAccessor(() => 23 ).value)
console.log(foo.value)
foo.value = 42

In short, the identifier is replaced with an object containing a single accessor called value. I may be erroneous in my illustrative example here, but it's a rough idea on how this could be implemented for a transpiler.

Jack-Works commented 2 years ago

what you have proposed has already been in the EXTENSION.md of this repo.

https://github.com/tc39/proposal-decorators/blob/master/EXTENSIONS.md#let-decorators

samholmes commented 2 years ago

I wasn't aware of these extensions. Good to know.

Looks like it covers most of what I mentioned in this issue. However, one other thing it doesn't cover and I haven't quite mentioned, is the idea to decorate the only return value of a function:

function name() @decorator {
  return value
}
const name = () @decorator => value

Unless I missed it, this would be worthwhile to add to consideration in the extensions. Having this could be useful to be able to decorate just the return value. Otherwise, the only way to decorate the return value would be to decorate the entire function declaration or assignment value for an identifier and this would require a transformation of decorator kinds:

const decoratorForAccessors = ...

// Will work
const @decoratorForAccessors foo = value

// WIll not work
@decoratorForAccessors
function foo() {
  // ...
}

// Will work if you wrap with some transformer
@applyToReturn(decoratorForAccessors)
function foo() {
  // ...
}
panjiangyi commented 2 years ago

I'm a big fan of decorater, But in mainstream Frontend frameworks, Class style code are not natively supported. Like React is functional, Vue is based on plain object. No handy way to use decorater if decorater just support Class.

Decorater definitely need support universal definition

trusktr commented 2 years ago

Yeah, decorators on other parts will be super handy in an add-on proposal later.

I want to throw out there that making reactive variables with let @decorator and using them in reactive functions with @effect function, or similar, may just be wonderful:

Example:

// count.js
import {signal} from 'really-awesome-lib'

// reactive variable (signal)
@readonly
let @signal count = 0

// count is readonly outside, but still writable within the module.
setInterval(() => count++, 1000)

@readonly modified the export binding that is used outside of the module, while @signal modified the variable accessor that is used within the module

// main.js
import {createEffect} from 'really-awesome-lib'
import {count} from './count.js'

// This automatically re-runs any time `count` changes (dependency-tracking reactivity).
// The reactive implementation tracks any signals accessed within the function (dependencies)
// in order to re-run the function when the signals change.
createEffect(() => {
  console.log('count:', count)
})

// later
count += 2 // readonly throws an error, count is not writable outside the module where it came from.
pzuraq commented 2 years ago

Closing as a duplicate of #306