Elixir function decorators

A decorator is a macro which is executed while the function is defined. It can be used to add extra functionality to Elixir functions. The runtime overhead of a function decorator is zero, as it is executed on compile time.

Examples of function decorators include: loggers, instrumentation (timing), precondition checks, et cetera.

Installation

Add decorator to your list of dependencies in mix.exs:

def deps do
  [{:decorator, "~> 0.0"}]
end

You can now define your function decorators.

Usage

Function decorators are macros which you put just before defining a function. It looks like this:

defmodule MyModule do
  use PrintDecorator

  print()
  def square(a) do
    a * a
  end
end

Now whenever you call MyModule.square(), you'll see the message: Function called: square in the console.

Defining the decorator is pretty easy. Create a module in which you use the Decorator.Define module, passing in the decorator name and arity, or more than one if you want.

The following defines a print() decorator which prints a message every time the function is called:

defmodule PrintDecorator do
  use Decorator.Define, [print: 0]

  def print(body, context) do
    quote do
      IO.puts("Function called: " <> Atom.to_string(unquote(context.name)))
      unquote(body)
    end
  end

end

Note that print() here is a function, not a macro! The actual macro has arity 0, and will be imported in the caller module. The decorator module's print() function gets called when the actual function is being defined.

The arguments to the decorator function are the function's body (the AST), as well as a context argument which holds information like the function's name, defining module, arity and the arguments AST.

Compile-time arguments

Decorators can have compile-time arguments passed into the decorator macros.

For instance, you could let the print function only print when a certain logging level has been set:

print(:debug)
def foo() do
 ...

In this case, you specify the arity 1 for the decorator:

defmodule PrintDecorator do
  use Decorator.Define, [print: 1]

And then your print() decorator function gets the level passed in as the first argument:

  def print(level, body, context) do
    # ...
  end