Nicest way to emulate function decorators?

Hi,

I am writing the Elixir integration for AppSignal, an application metrics solution. As of such, I am looking for a way to decorate functions in a developer-friendly way, so that these functions are automagically wrapped with library calls to measure the time it took to execute them (and send that info off to the backend).

As instrumenting functions is a common task while analyzing an application’s performance I want to minimize the amount of work the developer has to do to add instrumentation. As of such I have found two ways to do the instrumentation, both of which have its drawbacks:

1 - an instrumented do .... end block, in which you define the functions example here. Drawback of this method is that inside the block, everything is indented one level deeper, causing all the code to change while you have in fact just added 2 lines of code;making merging code changes harder;

2 - replace def foo() by def_instrument foo() (or similar) and use Kernel.def (like suggested here); the downside of this method is that it just “feels” weird to not read def, plus editor syntax highlighting breaks.

Are there any alternatives to tackle this? Ideally my solution would be a (Python / Java) decorator kind of syntax like this:

@instrumented
def foo(bar) do
  ...

But I am not sure that this is technically possible. I would love some input from the community on this!

cheers,
Arjan

6 Likes

I’d probably just make something that the user could use just via:

defmodule SomeUserModule do
  use Instrumented,
    funs [
      (:hello, 0)
      ]

  def hello, do: "world"

  # .. lots of more functions
end

In your use you can hook all the functions that are listed by the tuple of the funName and funArity and use a macro to hook the def’s and instrument the ones that match the list. That seems like the easiest way to hook that. Could do an :all or so for the funs to say to do everything

Or you could do something like this if you want to mark ‘at’ the function site:

defmodule SomeUserModule do
  use Instrumented

  @instrumented
  def hello, do: "world"

  # .. lots of more functions
end

Where that would basically do the same thing but you’d walk the ast looking for that attribute than an immediately following function declaration or so.

It would be entirely possible for someone to make a generic ‘function decorator’ macro module that works like that, could do things like @decorate SomeModule.functionWrapper or so before the functions. Unsure if anyone’s made one yet, but eh?

4 Likes

Yes this latest idea was how I envisioned implementing it, walking the AST looking for module attributes next to a def. However, I dont know where I should hook in to, @before_compile does not allow you to alter the AST, iirc… and __using__ only allows you to add code to the end of the module.

1 Like

What about the @on_definition hook?

You can always hook def itself within the module with a macro and have it either call to the normal def or do special things based on information too (that would work well with the first style especially), or at least using it for the second style and renaming the function (adding a NONINSTRUMENTED_ or so to everything) then setting up trampolines with instrumenting or not at the end based on the annotations.

1 Like

Yes, overriding def with a macro seems possible however I still don’t know how to get the @ attributes preceding the def, because I’m not sure there is a way to get the entire AST tree in some kind of hook and alter it.

I’m starting to lean to your first suggestion:

use InstrumentedDefs, foo: 2, bar: 3

Use a generic callback for def, when it gets called, check if @instrumented is set as a module-attribute, if so, do your stuff and unset it. If it is not set just do nothing.

There are some handy functions in the Module-module to work with module attributes ((get|put|delete|register)_attribute/*)

1 Like

Thanks. But I guess I also need to check its position in the AST - basically misuse module attributes to check whether an attribute occurs just before a def.

Maybe if I put the decorators inside the function as the first thing, I can get away with just overriding def:

def some_function(bar) do
  @instrumented
  # ... do stuff
end

Why do you need it to be the very last thing before the function or the very first in it? If you are too strict, it will be impossible to compose such annotations.

Also, why should it matter if one writes variant A or B?

A:

@instrumented
@doc "documented!"
def foo(bar), do: bar

B:

@doc "documented!"
@instrumented
def foo(bar), do: bar

Of course you can exchange @doc with @spec or any other function level attribute or combinations thereof.


PS: AFAIK you cant do just an empty attribute you need to assign something to it.

1 Like

It doesnt need to be strictly the last attribute before the def but the attribute should be scoped to the function somehow, e.g. some defs need instrumentation, others dont:

def foo(bar) do
  # not instrumented

@instrumented
def i_am_instrumented(bar) do
  # ...

def foo2(bar) do
  # not instrumented, again

I know about attribute definitions needing definition but inside the __using__ the attribute can get defined.

You have missed some of my points.

When you define @instrumented in the using, it will get replaced by that value in the sourcecode on every occurence, this can make your sourcecode invalid.

My proposal was to explicitely set it before functions, maybe specifying some extra stuff (similar to ExUnits @tags). And unset it in the callback.

1 Like

Ah! That makes sense suddenly.
So @instrumented “blablah” just before a def (which is an overridden macro); and then the macro does its AST magic, and unsets the attribute. Neat! I’ll try that.

Using an @ attribute does not work, I think, but creating a decorate() macro does, see this gist.

That syntax is pretty OK, if you ask me:

decorate()
def i_am_instrumented(bar) do
  ..

Next step: creating macros to create decorator macros :stuck_out_tongue:

iex> quote do
...> @instrumented
...> def hello, do: "world"
...> end
{:__block__, [],
 [{:@, [context: Elixir, import: Kernel], [{:instrumented, [], Elixir}]},
  {:def, [context: Elixir, import: Kernel],
   [{:hello, [context: Elixir], Elixir}, [do: "world"]]}]}

Nah, that is fine if it is only for the AST to go over, it only matters to set something if you want to access it post-ast.

You could always do something like this too:

defmodule SomeUserModule do
  use Instrumented
  @instrumented hello: 0
  @instrumented anotherFun: 4
  # ...

  def hello, do: "world"

  # .. lots of more functions
end

Hah, yep, could make a library for it all.

Doing just that! Right now I’m generating macros. Does not look so nice (no @) but it works pretty well. Let me know if you have a better way of doing this :slight_smile:

2 Likes

One this thread hasn’t really talked about is whether the decorator pattern is actually a good idea in Elixir. I’m not entirely convinced that it is.

With instrumentation for example very frequently the caller site is the thing that should care about whether or not a given function call is instrumented. If you mark a function as instrumented with a decorator then every call to it is gonna have some kind of side effect whether that is actually desired or not.

2 Likes

I partly agree with you… however, I use Java a lot which got my mind twisted in the wrong direction maybe :slight_smile:

The use cases that I see right now are:

  • For instrumentation, it is nice to use decorators because they allow you to turn it on or off with the addition/deletion of a single line of code.
  • I also can see it working for Phoenix controllers, a bit like putting a plug in a controller but only for a single function call.
  • Or for adding precondition checks to a function.

Anyway, I’ve wrapped it into a library: https://github.com/arjan/decorator

I think it was the :trace module could allow you to hook functions, so you could define things to instrument based on module name and function name from the config or so?

Also found a way to do it with @ but downside there is that it breaks “regular” module attributes…

How so?

Because I do import Kernel, except: [@: 1]

and from my own @ macro I cant seem to call the Kernel.@ macro