arjan

arjan

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

Marked As Solved

arjan

arjan

Also Liked

josevalim

josevalim

Creator of Elixir

This may be too late but you really really really shouldn’t copy Elixir’s private source. When you rely on Elixir internal details and then your code breaks when users of your code update their Elixir version, it gives a sense of instability to the ecosystem while we are working very hard to keep Elixir backwards compatible.

One suggestion is, when you are traversing the AST, you should see if that’s a decorated attribute and, if not, you should regenerate it as Kernel.@(ast) or something similar.

However, you should not need to traverse the AST to implement decorators. It should be possible to implement this by using @on_definition. Since @on_definition is going to be invoked before every function clause, you can collect information such as function body, arguments, etc and then use @before_compile to traverse all of the collected functions, calling the decorator, and emitting new clauses. You can also use defoverridable and tell decorators to simply invoke super when they need to call the parent function.

Here is an example implementation:

defmodule Decorator do
  defmacro __using__(_) do
    quote do
      @on_definition Decorator
      @before_compile Decorator
      @decorators %{}
      @decorator nil
    end
  end

  def __on_definition__(env, kind, fun, args, _guards, _body) do
    if decorator = Module.get_attribute(env.module, :decorator) do
      decorators = Module.get_attribute(env.module, :decorators)
      decorators = Map.put(decorators, {kind, fun, length(args)}, decorator)
      Module.put_attribute(env.module, :decorators, decorators)
      Module.put_attribute(env.module, :decorator, nil)
    end
    :ok
  end

  defmacro __before_compile__(env) do
    decorators = Module.get_attribute(env.module, :decorators)
    for {{kind, fun, arity}, decorator} <- decorators do
      args = generate_args(arity)
      body = decorator.decorate(kind, fun, args)
      quote do
        defoverridable [{unquote(fun), unquote(arity)}]

        Kernel.unquote(kind)(unquote(fun)(unquote_splicing(args))) do
          unquote(body)
        end
      end
    end
  end

  defp generate_args(0), do: []
  defp generate_args(n), do: for(i <- 1..n, do: Macro.var(:"var#{i}", __MODULE__))
end

defmodule InspectDecorator do
  def decorate(_kind, _name, args) do
    quote do
      IO.inspect super(unquote_splicing(args))
    end
  end
end

defmodule Sample do
  use Decorator

  @decorator InspectDecorator
  def add(a, b) do
    a + b
  end
end

# Should print 3
Sample.add(1, 2)

I don’t quite agree with the idea but I believe the implementation above would be better than relying on Elixir internals. The sketch above is also not complete, for example, we don’t support multiple decorators, but you should be able to change it by changing the on_definition implementation.

josevalim

josevalim

Creator of Elixir

Because it is very likely the pagination library should also be just using functions instead of a plug. I would rather do query |> Repo.paginate(params) or query |> paginate(Repo) than the plug or decorator approaches.

I can’t say about other languages but the fact validations in Ecto are not decorators makes them extremely easy to compose. If you need conditional validations, dynamic parameters, etc, you can just write Elixir code instead of finding the proper decorator incantation.

When it comes to composition and flow-control, there is no decorator that is going to be simpler and more readable than pattern matching, with and the |> operator (for the simplest cases).

NobbZ

NobbZ

Create a function which does your thing and use it.

Last Post!

hauleth

hauleth

The decorators have problem in form that they do not work nicely with multiple function definitions in case of pattern matching. Is this:

@decorate trace()
def sum([x | xs]), do: x + sum(xs)

def sum([]), do: 0

The same as this:

@decorate trace()
def sum([x | xs]), do: x + sum(xs)

@decorate trace()
def sum([]), do: 0

And this:

def sum([x | xs]), do: x + sum(xs)

@decorate trace()
def sum([]), do: 0

If yes, then how will you detect when to decorate function? If not then how it will not be confusing?

In case of logging and instrumentation I think the best solution will be just define function like:

def trace(name, func) do
  ctx = start_trace(name)
  try do
    func.()
  after
    end_trace(ctx)
  end
end

And use it like that:

def my_traceable_func() do
  trace(:my_traceable_func, fn ->
    # do thing
  end)
end

Alternatively you can use macro to extract some more information at the compile time.

Where Next?

Popular in Questions Top

RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 131117 1222
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New

We're in Beta

About us Mission Statement