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
Also Liked
josevalim
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
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
Create a function which does your thing and use it.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








