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

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
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 record...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
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
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
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
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement