Last Updated: September 27, 2019
·
1.798K
· abe33

Python-like decorators with CoffeeScript

Decorators are a powerful python feature, they are just regular functions that takes another function as argument and can either wrap the function call in another function or simply add metadata to the function itself.

Javascript can't express decorators as fluently as Python, the latter
provides a specific syntax to decorate functions. However decorators can greatly help dryifying code and can be expressed quite elegantly in CoffeeScript.

The general rule is that if your decorator wrap the passed-in method, the decorator should be placed on the right side of the assignement, taking the created function directly as argument and then affecting the wrapper function to the left, as follow:

f = decorator -> 

If your decorator only decorate the passed-in function with metadata, the decorator can be placed before the left side of the assignment:

decorator 'data', 
f = ->

Examples of decorators in functional style coffeescript:

bold = (f) -> -> "<b>#{f.apply null, arguments}</b>" 
italic = (f) -> -> "<i>#{f.apply null, arguments}</i>" 

sayHello = bold italic -> 'Hello World!'

sayHello() # '<b><i>Hello World!</i></b>'

The code above use decorators to add html formatting to a function return.
Note the double arrow that indicate a partial function.

#  generator that creates a decorator for a given meta 
meta = (property, list=false) -> 
  if list?
    (value..., target) -> target[property] = value; target
  else
    (value, target) -> target[property] = value; target

# the concrete decorators that will be exposed
aliases = meta 'aliases', true
describe = meta 'description'
environment = meta 'environment'
usages = meta 'usages', true

# decorator usage, the comma at the end force coffeescript
# to evaluate the following expression as wrapped functions calls
aliases 'my:command',
environment 'production',
usages 'cli my:command [arguments]',
describe 'The command description',
myCommand = (args..., callback) ->
  # ...

myCommand.aliases     # ['my:command']
myCommand.description # 'The command description' 

The code above define a basic async cli command where the command metadata are defined directly on the function.

Decorators in oo style coffeescript:

Wrapper type decorator:

class Module
  @cachableMethod: (f) -> 
    # the cache is defined locally, avoiding collisions
    # with other cached functions 
    __cache__ = {}
    return ->
      key = (a for a in arguments).join(';')
      return __cache__[key] if __cache__[key]?
      __cache__[key] = f.apply this, arguments

  cached1: @cachableMethod -> arguments
  cached2: @cachableMethod -> arguments

module = new Module
module.cached1 10     # cache1 = {10: [10]}
module.cached1 20, 30 # cache1 = {'10': [10], '20;30': [20,30]}
module.cached2 'foo'  # cache2 = {'foo': ['foo']}

Descriptor type decorator:

 class Module
  @methodMeta: (property) -> (value, target) ->
    # target will be an object as these decorators
    # will be placed before the declaration
    for k,v of target
      v[property] = value
      # the function is added on the prototype by the decorator
      @::[k] = v
    target

  @describe: @methodMeta 'description'
  # a zero arity decorator
  @proxyable: (target) ->
    for k,v of target
      v.proxyable = true
      # the function is added on the prototype by the decorator
      @::[k] = v 
    target

  # Python provides a specific syntax for decorators
  # that coffee can't follow, then decorators with 
  # zero arity can't be placed on their own line.
  @proxyable @describe 'The function description',
    describedMethod: ->
      # ...

  normalMethod: ->

module = new Module
module.describedMethod.description # 'The function description'
module.describedMethod.proxyable   # true