Skip to content
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

Decorators for all definitions #440

Closed
samholmes opened this issue Jan 17, 2022 · 6 comments
Closed

Decorators for all definitions #440

samholmes opened this issue Jan 17, 2022 · 6 comments

Comments

@samholmes
Copy link

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 samholmes reopened this Jan 17, 2022
@samholmes
Copy link
Author

samholmes commented Jan 20, 2022

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:

  • get retrieves a cached value
  • update the original accessor's get function (the assignment expression)
  • set changes the cached value and invokes any dependency accessor's update method (the dependency graph is conceivably maintained and constructed by the runtime; details spared for brevity, but refer to ironjs.org for an example implementation).

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
Copy link
Member

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
Copy link
Author

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
Copy link

panjiangyi commented Mar 18, 2022

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
Copy link
Contributor

trusktr commented Mar 25, 2022

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
Copy link
Collaborator

pzuraq commented Mar 27, 2022

Closing as a duplicate of #306

@pzuraq pzuraq closed this as completed Mar 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants