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

Idea: syntax for decorator composition. #515

Open
trusktr opened this issue Oct 2, 2023 · 3 comments
Open

Idea: syntax for decorator composition. #515

trusktr opened this issue Oct 2, 2023 · 3 comments

Comments

@trusktr
Copy link
Contributor

trusktr commented Oct 2, 2023

Right now, composing decorators is much too cumbersome. You have to write conditional branches inside of a new function to handle all the kinds of decorators, and often you repeat logic, and can get it wrong.

Rather than show how to compose decorators by using multiple functions inside a new function, which I'm sure you're all familiar with, let me show the desired feature:

Currently:

class MyClass {
  @cool @logged foo = 123
  @cool @logged bar = 123
}

Idea (totally random syntax choice, but to show the idea):

let @cool2 = @cool @logged

class MyClass {
  @cool2 foo = 123
  @cool2 bar = 123
}

This achieves the same thing as composing both cool and logged functions together inside a new cool2 function, but in a much simpler way for the author of the composed decorator.

@trusktr trusktr changed the title Proposal: syntax for decorator composition. Idea: syntax for decorator composition. Oct 2, 2023
@pabloalmunia
Copy link
Contributor

It is very suggestive, but I have some comments:

  • Let's remember that a decorator is a function that is applied as a decorator, it is not a specific artifact. Such a syntax should be applied as a generic composition of functions, not as something specific to functions used as decorators. const func3 = func1 func2.

  • A decorator can be evaluated in two steps, i.e., first a function is executed that returns the decorator function, as in @logger('warning'). That syntax complicates this behavior a bit.

  • It is very easy to build a function that combines decorator functions into one. It is not necessary to define a specific syntax:

const Mixer = (...decorators) => 
  (element, descriptor) => 
    decorators.reverse().reduce((element, decorator) => decorator(element, descriptor) || element, element);

let cool2 = Mixer(cool, logged);

@trusktr
Copy link
Contributor Author

trusktr commented Nov 20, 2023

@falentio Yeah, my syntax idea was not very good. 😄

@pabloalmunia That Mixer idea looks like it is on the right track, but here's a TS playground showing that the result of Mixer is different than with plain decorators for class fields. Looks like class field decorators are supposed to always receive undefined for their value parameter.

Here's a Babel repl showing the same issue with Mixer (make a whitespace modification to make it run the code).

Even with some adjustments to Mixer, it is definitely not as simple as @one @two @three where decorator syntax has all the semantics baked right in.


About syntax, what if it were

const composed = @cool @logged('warning')

class MyClass {
  @composed foo = 123
  @composed bar = 123
}

where composed is now a function reference?

In the future when/if function decorators come out,

// this would not compose, but would decorate the function
const someFunc = @cool @logged('warning') function() {...}

// this would compose
const composed = @cool @logged('warning')

const otherFunc = @composed function() {...}

Would it perhaps just be a decorator composition expression? Maybe when decorators are written out without decorating something, they just create a composed function.

Example 1:

// this would compose
const composed = (level) => @cool @logged(level)

const otherFunc = @composed('warning') function() {...}

Example 2:

function foo(decorator) {
  return @decorator function() {...}
}

foo(@cool @logged('warning'))

Example 3:

console.log(typeof (@one @two @three)) // "function"

@trusktr
Copy link
Contributor Author

trusktr commented Nov 20, 2023

Use Case

A library wants to import decorators, and compose them into new ones to export the new ones. For example, for a custom element library, it might do this:

import {reactive} from 'some-reactive-library' // @reactive decorator makes a class field reactive

// _attribute decorator implementation
function _attribute() {
  // ... map HTML attribute to JS field ...
}

/** A decorator that does two things: maps attributes to JS properties, and make properties reactive. */
export const attribute = @_attribute @reactive

End user:

import {createEffect} from 'some-reactive-library'
import {customElement, attribute} from 'some-custom-element-library'

@customElement('my-el')
class MyEl extends HTMLElement {
  @attribute name = "Batman"
}

const el = new MyEl()
document.body.append(el)

createEffect(() => {
  // This re-runs any time `name` changes because `@attribute` is composed with `@reactive`
  console.log(el.name)
})

el.setAttribute('name', 'Superman') // triggers the effect, logs "Superman" to console

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

2 participants