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.

josevalim

josevalim

Creator of Elixir

Here is the issue I see in your code. Imagine that the parameter you need to validate depend on some state in socket. Maybe it depends if the user is in their phone or if they are an admin. How are you going to apply this conditional validation? Maybe you need to add more sugar to defparams?Or maybe you need to define now two private functions with different behaviours that you’d call accordingly?

If this code was just Elixir code then the answer to how you would change any of this code is immediately obvious. You would handle it as any Elixir code. You would use pattern matching or conditionals, etc.

The type of conditional validation you are expressing can be done with functions and data types. There is no need for decorators or indirection:

  @types %{
    status_id: :integer,
    reaction_type: :string
  }
  def status_react(params, socket) do
    with {:ok, ~m{status_id, reaction_type}} <- validate_params(params, @types),
         do: ...
  end

You can use with or you can also use something like changesets, where you validate and annotate the types as you go.

OvermindDL1

OvermindDL1

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?

Where Next?

Popular in Questions Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement