Skip to content
This repository has been archived by the owner on Feb 19, 2018. It is now read-only.

CS2 Discussion: Features: decorators #9

Closed
rattrayalex opened this issue Jul 23, 2016 · 51 comments
Closed

CS2 Discussion: Features: decorators #9

rattrayalex opened this issue Jul 23, 2016 · 51 comments

Comments

@rattrayalex
Copy link
Contributor

rattrayalex commented Jul 23, 2016

Decorators are an increasingly popular feature of ES6, particularly with React development.

However, they use @atSyntax, which clashes with coffeescript's this. shorthand.

We could:

  1. Come up with an alternative syntax for decorators
  2. Come up with a new syntax for this. shorthand
  3. Keep both as they are and just work hard to parse them
  4. Get rid of this. shorthand
  5. Don't add decorators
  6. Any other ideas?

Thoughts?

@carlmathisen
Copy link

It is unfortunate that ES6 generator uses @ as well, but we should try to keep as much BC as possible - especially when it comes to basic stuff like this shorthand that literally everyone uses.

You're probably right, it's hard to parse and distinguish between the two if decorators were to keep the same symbol.

class A
  @decorator
  func: ->

Granted, when transpiled today it doesn't make sense - @decorator just becomes A.decorator;, but it is still valid JS. Who knows, it might even trigger a getter function in someone's app. Framer JS, for instance, heavily uses function getters for their properties with Object.defineProperty. https://github.com/koenbok/Framer/blob/master/framer/BaseClass.coffee#L20

So your first list item, come up with an alternative syntax for decorators, is probably the best solution. Maybe use * since it's an addition of information and functionality, but not sure if mixing in an operator is a good idea.

@rattrayalex
Copy link
Contributor Author

rattrayalex commented Jul 25, 2016

Sorry, BC?

EDIT: ah, Backwards Compatible. Yes, I'm increasingly coming around to that.

@rattrayalex
Copy link
Contributor Author

rattrayalex commented Jul 25, 2016

Let's brainstorm other ideas...

class A
  @decorator
  func: ->
class A
  %decorator  # maybe?
  func: ->
class A
  ^decorator  # I don't think so
  func: ->
class A
  &decorator  # actually, not bad – I like that it implies you're adding something
  func: ->
class A
  *decorator  # perhaps too similar to generator syntax? But I don't think actually problematic.
  func: ->

Any other ideas?

@carlmathisen
Copy link

Regarding *, it's good point that it might be confused as a generator.

Btw, CoffeeScript's official docs on generator functions:

CoffeeScript functions also support ES6 generator functions through the yield keyword. There's no function*(){} nonsense — a generator in CoffeeScript is simply a function that yields.

@JimPanic
Copy link
Contributor

JimPanic commented Jul 25, 2016

@carlmathisen That's a good point re yield! It might be a good idea to keep this as-is for now and make sure the current syntax works with ES6 generators.

Edit: whoops, this is the thread about decorators. Sorry. :)

@JimPanic
Copy link
Contributor

I've just looked at decorators in Python, Elixir and ES6. They all use @decorate syntax. Lua doesn't seem to have a special decorator syntax as far as I understood it (http://lua-users.org/wiki/DecoratorsAndDocstrings).

In the long run, it might be a good idea to find a different shorthand for this. and deprecate its current incarnation in favour of a "standard" decorator syntax embraced in quite a few languages and tools. The current CS syntax is heavily inspired by Ruby in this regard, which (as far as I can tell) doesn't sport syntax-backed decorators at all and has a vastly different stance on classes, modules and methods than JS does.

@carlmathisen
Copy link

carlmathisen commented Jul 25, 2016

Although I understand the wish for a "standard" decorator syntax, I disagree with deprecation of @ shorthand for this. Decorators are merely a nifty feature, and shouldn't take a driver's seat on how a language is designed. The @ for this isn't just a random symbol either, it has a semantic meaning to it and it is very easy to automatically understand what it means.

If we decide to reserve a word or symbol for decorators, it could just as well be decorate. This might break some apps having a variable named decorate, but it's better than breaking 100% of CS code with @.

Example:

class A
  decorate readonly, deprecated
  func: ->

Btw, here is a discussion on decorators over at CoffeeScript's own issue tracker: jashkenas/coffeescript#76. Interesting read, even though the issue is quite old.

@JimPanic
Copy link
Contributor

JimPanic commented Jul 25, 2016

Decorators are merely a nifty feature, and shouldn't take a driver's seat on how a language is designed. The @ for this isn't just a random symbol either, it has a semantic meaning to it and it is very easy to automatically understand what it means.

You're probably right. 👍

Thinking about your proposal and how decorators seem to be implemented (and without having read #76), are we not be able to use decorators right now, without the keyword?

readonly = (fn) -> ((...) -> ...)

decorated_fn = readonly (foo) -> foo

I used this type of function "decoration" before. So introducing a keyword like decorate for language supported decorators is certainly a better option than what I proposed.

@carlmathisen
Copy link

Your suggestion is basically what Jeremy says in #76 (tldr: no need for language support, just create a wrapper function)

@JimPanic
Copy link
Contributor

On 25 Jul 2016, at 22:06, Carl Mathisen [email protected] wrote:

Your suggestion is basically what Jeremy says in #76 (tldr: no need for language support, just create a wrapper function)

Ah, cool. But there might be implications that this doesn't cover. I don't know how far language support is adding additional features in ES201*. I'm pretty sure any distinction for decorators is a good thing to be able to create proper tools to use this feature.

@dadleyy
Copy link

dadleyy commented Jul 28, 2016

I know coffeescript today is against adding them but regarding getters and setters -- would decorators be used for this? If not, is it worthwhile opening a new issue and having the discussion there or is it assumed that they won't make it into this new project? Personally I really enjoy using getters in browser-side UI code. I have found it helpful to keep track of arbitrary business logic rules (a user can do this, can't do this, etc...) by writing a flags getter on some of my component delegates/services/controllers:

class DietManager {
  get flags() {
    let {meals} = this;
    let total_calories = 0;
    for(let i = 0, meal_count = meals.length; i < meal_count; i++) {
      total_calories += meals[i].calories;
    }
    return {total_calories};
  }
}

At least, I think the above is a good example of a use for getters.

@rattrayalex
Copy link
Contributor Author

@dadleyy

is it worthwhile opening a new issue and having the discussion there

Yes, please do! getters/setters would likely be an appropriate issue title.

@carlsmith
Copy link

carlsmith commented Jul 28, 2016

-1 on this. CoffeeScript already has decorators. ES3 already has them too. Every language with first-class functions supports decorators. What ES6 is adding is just syntactic sugar for passing a function to a decorator. CoffeeScript already has syntactic sugar for doing this, and it literally couldn't be improved on. You just put any expression (typically a name) that evaluates to a decorator in front of the function expression you want to decorate. If you have a decorator named memo, you can just put memo in front of anything you want to decorate.

# regular square function
square = (x) -> x * x
# memo decorated square function
square = memo (x) -> x * x

We don't need ES6 sugar in CoffeeScript. That's the whole point of CoffeeScript.

Decorators are an awesome feature, but every language already has them. It's just that some languages make applying them awkward. Python without special syntax:

def square(x): return x * x
square = memo(square)

Python with special syntax:

@memo
def square(x): return x * x

There's absolutely no need for special syntax in CoffeeScript. If we had to use parens for invocations, it'd be different:

square = memo((x) -> x * x)

From experience, I can say confidently that you never need those parens in practice. You can always use the name of a decorator as if it's a prefix operator.

Things are less pretty if you use functions that return decorators, but it still works well enough. You just wrap the decorator expression in parens:

square = (memo 10) (x) -> x * x

That could be a little bit prettier with special syntax, for example:

@memo 10
square = (x) -> x * x

But, it's a pretty big change to add minor convenience to a rare construct, and the new syntax would be awful when using functions as args:

employees.forEach(
    @memo 10
    (each) -> console.log each
)

That's a mess, especially when we already have:

employees.forEach (memo 10) (each) -> console.log each

Decorators never nest any deeper than that. You always either use a decorator, or call a function that returns a decorator (so you can parameterise the decorator). They are the only two forms decoration takes.

@rattrayalex
Copy link
Contributor Author

@carlsmith have you used es6 decorators? The examples you provide are not valid (they can't be user on top level functions).

How can one decorate a class in coffeescript with equivalent elegance to es6?

@carlsmith
Copy link

You put the names of the decorators in front of the class. Assume singleton is a class decorator:

Rabbit = singleton class extends Animal
  constructor: (name) ->
    @name = name

@carlsmith
Copy link

carlsmith commented Jul 28, 2016

There's no such thing as ES6 decorators. There's only ES6 decorator invocation syntax, so it's really just a question of what's the most elegant way to pass a function literal or class literal to another function. There's no such thing as a top-level function in CoffeeScript either. All functions are expressions.

@carlsmith
Copy link

carlsmith commented Jul 28, 2016

People need to keep in mind that a decorator is just a normal, higher-order function, like map or filter. Decorators are a functional programming pattern. Languages can add sugar to make using the pattern prettier, but it is only sugar. If a language supports first-class functions, then it supports decorators, pretty much by definition.

Most languages require that the arguments for any invocation are wrapped in parens, so passing a function or class literal to the decorator requires the literal to be wrapped in parens.

let square = deco(function(x) { return x * x });

Many languages allow (e.g. JavaScript) or require (e.g. Python) you to use function statements. A statement is not an expression, and can not be directly passed to a decorator as an argument. You must write the function first, then pass a reference to it to the decorator afterwards, assigning the result back to the original name.

function square(x) { return x * x }
square = deco(square);
def square(x): return x * x
square = deco(square)

In CoffeeScript, we can always prepend a decorator expression (typically a name) to a function or class literal.

square = deco (x) -> x * x

In other languages, they provide the decorator invocation syntax to work around limitations that CoffeeScript doesn't have (we don't have to wrap an argument in parens, and we don't have function statements).

@deco
def square(x): return x * x

That syntax also allows you to pipeline decorators.

@deco1
@deco2
def square(x): return x * x

In CoffeeScript, it's still a one-liner.

square = deco1 deco2 (x) -> x * x

Parameters

The @-based sugar allows you to use an invocation that returns a decorator, and decorate the new function with the result.

@decoWithArg(arg)
def square(x): return x * x

In CoffeeScript, you can easily do the same thing. You just need a pair of parens to keep things together properly.

square = (decoWithArg arg) (x) -> x * x

CoffeeScript's decorator invocation syntax works much better than @deco when applied to expressions. This is what CoffeeScript would look like if we accepted the new proposal:

employees.forEach(
    @memo 10
    (each) -> console.log each
)

This is what we have currently:

employees.forEach (memo 10) (each) -> console.log each

In its defence, the @-based syntax was never designed to be applied to expressions. It's designed for function statements, which CoffeeScript correctly removed as one of JavaScript's Bad Parts™.

@carlsmith
Copy link

carlsmith commented Jul 28, 2016

Just wanted to add a massive thank you to @rattrayalex for setting this discussion up, and for moderating all of these issues. There's a lot of technical conversation taking place across a growing pool of threads, and while it's stuff we all love getting into, it must be a challenge to stay on top of everything. It's a valuable conversation that we need to have about the future of CoffeeScript, and your enthusiasm and the hard work you've put into hosting this exchange are greatly appreciated mate.

@JimPanic
Copy link
Contributor

@carlsmith I can only second that! Thanks @rattrayalex!

@JimPanic
Copy link
Contributor

JimPanic commented Jul 29, 2016

Those are very good points, @carlsmith.

To illustrate something like typespecs and inline (decorated) docs working already:

square =
  doc "squares a number and returns the result",
  typespec "(number): number",
  memoize (x) ->
    x * x

…will result in:

var square;

square = doc("squares a number and returns the result", typespec("(number): number", memoize(function(x) {
  return x * x;
})));

square = doc("", typespec("(number): number", function(x) {
  return x * x;
}));

Does look quite neat to be honest. 👍

I used higher-order functions as decorators before in Coffeescript, but sincerely thought the ES6 standard had some more features than just another invocation syntax.

@carlsmith
Copy link

carlsmith commented Jul 29, 2016

@JimPanic - Nice! I've never seen it done quite like that before. That's really cool. I'll be ripping that idea off at some point!

There's a similar thing where you define a collection of functions that can take normal args or a hash that meets some criteria, and always return a hash that meets the criteria. Then you can pass args to any of the functions, then pass the result (recursively) to other functions in the collection:

file = openInEditor writeToDisk newFile "foo.coffee" # process works from right to left

In practice, the names will normally be shorter:

file = edit set create "foo.coffee"

Your example is different and more clever though still. Mixing the patterns from functional programming with the stuff we normally do with objects and impure functions never gets tired :)

@JimPanic
Copy link
Contributor

@carlsmith Thanks! Be my guest :)

I used your outlined approach as well a few times; but I can't find it anymore online. I used it to describe properties for objects (readonly yaddayadda 'foo', object). That code might be lost in a backup though.

@carlmathisen
Copy link

carlmathisen commented Jul 29, 2016

I must admit I haven't used decorators that much, so I'm loving the latest development in this thread. I'm definitely sold on:

file = openInEditor writeToDisk newFile "foo.coffee" # process works from right to left

One thing though, consider this example from http://github.com/wycats/javascript-decorators

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

I've seen such class method decorators in the React community as of late.

This works just fine today with CoffeeScript's classes:

class Person
  name: readonly ->
    return "#{@first} #{last}"

But how could we translate this to ES6 class methods in CoffeeScript, though? The key/value way of setting up functions in prototype objects doesn't apply in a ES6 class since it has a special function syntax like above (name(){} instead of name: function(){}).

@dadleyy
Copy link

dadleyy commented Jul 29, 2016

@carlmathisen thats why (specifically the concept of a readonly decorator) I think we need getters/settings in cs6 so badly (moreso getters imo - I do think there is a good argument against side effects in setters but thats for another topic); your example could become:

class Person
  get directory_name: -> "#{@last}, #{@first}"

johnny = new Person "johnny", "appleseed"
console.log johnny.directory_name # logs "appleseed, johnny"

I don't think this covers all of the options available in object descriptors though, but a property defined on an object having only the get method is at least readonly in the sense that:

let user = {
  get directory_name() {
    return "appleseed, johnny";
  }
}

console.log(user.directory_name) // logs "appleseed, johnny"
user.directory_name = null
console.log(user.directory_name) // still logs "appleseed, johnny"

@rattrayalex
Copy link
Contributor Author

Some really great discussion here.

Regarding the class question, for something like this:

@connect(someStore)
@pureRender
export default class MyComponent extends React.Component {
  // ...
}

@carlsmith it sounds like you're suggesting this:

export default MyComponent = connect(someStore) pureRender class MyComponent extends React.Component
  # ...

and one could also do this:

class MyComponent extends React.Component
  # ...
export default connect(someStore) pureRender MyComponent

I suppose the pattern above really isn't so bad – the only downside is that it appears that all the decorators and the entire class decoration must appear on one line (testing with MyThing = wrap wrap2 class MyThing extends AnotherThing at coffeescript.org). That could become cumbersome; it sucks when you have no choice but to have a 100-character line of code.

Any ideas how that could be resolved?

Either way, I'm leaning towards concluding this thread with "no syntax sugar for decorators in CS6", with the hope that we add some helpful examples with the abovelisted patterns.

@rattrayalex
Copy link
Contributor Author

Oh, and thanks very much @carlsmith and @JimPanic for the kind words 🙂 it's very nice to hear.

If you or anyone else has feedback for how I've been conducting things so far, please reach out (anonymously, if you like, via mailbox service) - email is in my github profile.

Big thanks to all those who have been contributing so much of their own time and energy thus far!

@carlsmith
Copy link

carlsmith commented Jul 30, 2016

@rattrayalex - Exactly. There's a couple of ways you can break up the longer lines when you're using decorators, but yeah, the way you did it is correct. It can get a bit wordy in some cases, as you showed, it's not unrealistic to end up with export default on class with a couple of decorators with args etc., but it's not that common, and relatively simple to break down. Another option is to just use parens:

export default deco0 deco1(
  class extends Animal
)

If you just think of decorator syntax like...

@deco
function square(x) { return x * x }

...as just sugar for...

function square(x) { return x * x }
square = deco(square);

...it's kind of as though the @deco part is an invocation that wraps everything afterwards in parens, but the name is moved to the front as well:

square = deco(
function (x) { return x * x }
)

From there, it's fairly obvious which different ways you could possibly express the same thing.

@msuarz
Copy link

msuarz commented Mar 14, 2017

sorry i was not clear i meant using the symbol notation from ruby to indicate decorators eg:

class A

:decorator
func: ->

@ArmorDarks
Copy link

@carlsmith Some very nice thoughts, thanks! I never thought about CS expressions this way.

@sonhanguyen
Copy link

sonhanguyen commented Apr 17, 2017

Hi everybody, great discussion here so far. There is, however, one small thing that hasn't seem to be mentioned that I think worth factoring in.
Even though coffeescript sort of already has decorators in the form of nested function call, many think it doesn't suffice as they are forced to either write one liners or use nested parentheses which suck. With an existing trick you can actually write:

func = # one benefit of multi liners is that you have more room for comment
   decorate \ # the trick is the backslash
   decorateMore(withSome, params) \ # still need parentheses here but at least they are not nested
   (actual) -> body

Whether that means problem solved or decorators are still to be desired, I leave it to you guys.

@duanefields
Copy link

Just my two cents, that decorators are awesome, and are going to be increasingly popular. Mobx being the most visible to my work, so I feel like this is a must have for CoffeeScript 2

@carlsmith
Copy link

Is there anything anyone wants to be able to do with decorators that requires new syntax?

@factoidforrest
Copy link

still no motion on this? Makes me feel like coffeescript 2 is DOA if it is missing features from ES7 that make it incompatible with existing libraries

@GeoffreyBooth
Copy link
Collaborator

Decorators are still only a Stage 2 proposal. Any libraries that are already using them are very premature.

@factoidforrest
Copy link

factoidforrest commented Nov 30, 2017

I wonder if it would be possible to use backticks to pass raw javascript for the @decorator

@carlmathisen
Copy link

carlmathisen commented Nov 30, 2017

@light24bulbs I believe that would work, see the online CS repl: http://coffeescript.org/#try:class%20A%0A%20%20%60%40decorator%60%0A%20%20func%3A%20-%3E%0A%20%20%20

Although semicolons after decorators aren't a part of the spec, it should still work.

@aminland
Copy link

Wouldn't it be great if we could just do stuff like this? It would open up a world of possibilities...

+|requireLogin
+|registerRoute path: '/basePath'
class SomeComponent
    +|someDecorator
    @someStaticMethod: () -> ...

    +|registerRoute path: '/subPath'
    +|injectArgs user: () => @props.user
    someCallback: ({user}, evt) -> ...

@YamiOdymel
Copy link

@aminland Cool, the syntax reminds me of API Blueprint.

2017-12-11 1 01 28

@factoidforrest
Copy link

factoidforrest commented Dec 10, 2017 via email

@sonhanguyen
Copy link

one thing I'd like to add is that if we do, I really hope it'd be like python decorators which you can apply to any function, not like es6/java decorators which are restricted to methods.

@factoidforrest
Copy link

factoidforrest commented Dec 10, 2017 via email

@sonhanguyen
Copy link

@light24bulbs decorators do work on class level and a class is basically a function with extra semantic.

@GeoffreyBooth
Copy link
Collaborator

Hey folks,

A few points:

  • Our policy on new ECMAScript features is here. In short, we generally don’t add a feature to CoffeeScript until its syntax is finalized in ECMAScript; or we add it but output it using non-experimental syntax. We added support for object destructuring/rest syntax in this fashion, where we output the same polyfill as Babel while we wait for the feature to reach Stage 4.
  • If anyone wants to implement decorators and output the result as Stage 4 ECMAScript, please go right ahead. Such a PR would be welcomed.
  • @carlmathisen is right, you can use decorators now with backticks and Babel.
  • Node supports decorators?

@MadcapJake
Copy link

I've been thinking about decorators in CoffeeScript as I wanted a way for a base class to assemble a map of events to methods (for a child class) in a DRY fashion

When you place anything in front of a method body in CoffeeScript, the compiler will convert it to a property on the class prototype. So I had to use @:: due to the assignment of the method not actually happening inside of the class body.

class EventManager
	constructor: -> @events ?= {}
	via: (evt, fn) -> (@events ?= {})[evt] = fn.bind(this)
	trigger: (event) -> @events[event]?()

class Task extends EventManager
	completed: no
	toggleCompleted: @::via 'done', ->
		@completed = yes
		console.log "Toggling completed!"

t = new Task()
t.trigger 'done'

Just some food for thought that method decorators from a parent class will require the use of :: to reference.

@coffeescriptbot
Copy link
Collaborator

Migrated to jashkenas/coffeescript#4917

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests