MadsBoydMadsen

MadsBoydMadsen

I’m working on a macro to support automatic timing for functions.
If I have this - say:

def double(x) do
  x + x
end

I would like to be able to change it to this on a needs basis:

def_timed double(x) do
  x + x
end

I’ve got the basics of a macro written that can handle this:

defmodule Cw.Utilities.Timing.DefTimed do
  defmacro def_timed({name, _meta, args} = _ast, do: body) do
    arg_vars = Enum.map(args, fn {arg_name, _, _} -> arg_name end)

    quote do
      def unquote(name)(unquote_splicing(args)) do
        Timer.wrap_timed(
          fn unquote_splicing(arg_vars) -> unquote(body) end,
          [unquote_splicing(arg_vars)],
          unquote(name)
        )
      end
    end
  end
end

defmodule Timer do
  def wrap_timed(fun, args, name) do
    start_time = System.monotonic_time(:millisecond)
    result = apply(fun, args)
    end_time = System.monotonic_time(:millisecond)

    IO.puts("""
    [TIMED] Function: #{name}
    [TIMED] Arguments: #{inspect(args)}
    [TIMED] Execution time: #{end_time - start_time} ms
    """)

    result
  end
end

This works fine for functions with simple signatures as above.
But it doesn’t work when the signatures includes keyword-list, optionals, guards or pattern-matching.

I sense, that this must be a solved problem, but I cannot find anything online to demonstrate how to do it.
I’m looking for help to work it out.

NOTE: One of my colleagues have just pointed out, that macros should only be used sparingly (Meta-programming anti-patterns — Elixir v1.20.2).
My use-case strikes me as being pretty perfect for a macro, as adding and removing instrumentation would be a breeze (good luck to me :smiley: )
Is there a better - and equally straight forward - way for me to achieve what I’m trying to do ?

Showing Posts 1 to 10

rvirding

rvirding

Creator of Erlang

A very simple question: what does the expanded macro code look like? This might help understanding what is going on. Btw I don’t know. :wink:

MadsBoydMadsen

MadsBoydMadsen OP

Thanks for your interest and attempt to help :slight_smile:

Doing this for the above example: quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.puts
I get:

def double(x) do
  Timer.wrap_timed(
    fn x -> x + x end,
    [x],
    :double
  )
end

The problem I have does not immediately relate to the generated AST.
Rather it relates to the first parameter to the macro: {name, _meta, args} = _ast

The args is a representation of the formal arguments to the function (including simple arguments, keylist, optionals, guards and pattern-matches), but in a form that cannot immediately be unquoted back into a form that is syntactically correct in the context of an Elixir function-definition.

Rather it contains a bunch of meta-data scattered out through the structure. So, what I’ve been looking for - specifically - is a robust way to transform args to a syntactically valid form.
However, I recognise, that I may be attacking the problem the wrong way, so I attempted to state the problem in a form that was open to other solutions.

billylanchantin

billylanchantin

I’m honestly don’t think this is a solved problem largely for the reasons you note: the forms the AST can take are quite varied. While Elixir does give you the ability to manipulate AST directly, it doesn’t give you many nice helpers like Macro.find_the_do_block_in_this_ast/1.

(I’m speculating here, but I imagine it’s because the AST is an implementation detail subject to change.)

My personal experience with writing macros is that you’re on your own somewhat. The Meta-Programming chapters of the docs are quite good. But for specific applications, you often need to dig in and see what the expressions you plan to work with happen to come out as in AST form.

Note: another snag you’ll hit is that there are other blocks that come after the do block:

def reciprocal(x) when is_integer(x) do
  1 / x
rescue
  ArithmeticError -> :infinity
end
MadsBoydMadsen

MadsBoydMadsen OP

This is very helpful information. Thank you.

I will abandon the project and find a more appropriate way to arrive at my destination.
Thanks again :slight_smile:

garrison

garrison

Couldn’t you just re-use the function signature as-is and then wrap the body in an anonymous function and simply close over the arguments?

Like:

def double(x) do
  Timer.wrap_timed(fn ->
    x + x
  end)
end

BTW you should look into :timer.tc/1.

billylanchantin

billylanchantin

Just to clarify: I think the approach will work once you cover all the edge cases. I was mostly pushing back on this:

However I agree with @garrison. For most instances when I’ve needed to time functions, it was either for a one time thing or as part of a benchmarking effort.

garrison

garrison

Looking back at my reply I now see it was ambiguous, so just to clarify: the snippet I posted was meant to be the macro output. I was suggesting it would be easier to write a macro which makes no attempt to pass its arguments through the anonymous function and instead just closes over them.

Then the “edge cases” shouldn’t be so hard as the final “def” call will be almost exactly the same as your “deftimed” except you just wrap a bit of the AST in a fn -> unquote(...) end block.

I do agree the use-case here is a bit strange but sometimes people do things just to experiment so I won’t judge :slight_smile:

billylanchantin

billylanchantin

Ah got it. Yeah agreed a closure would probably be easier to implement since you can just change :def_timer to :def and wrap the do block. (I think there is still the edge case of the after block but no one uses those so who cares. :stuck_out_tongue:)

garrison

garrison

Honestly I forgot def even supported more blocks, I don’t think I’ve ever seen any of them used.

Another approach would be to mangle the function name and then generate another one which wraps it with the Timer call. Stack traces would be ugly, though.

al2o3cr

al2o3cr

The tricky part is that a function doesn’t even need to name its arguments if it pattern-matches on them, for instance:

def foo([a | _], %{wat: b}) do
  ...
end

Replacing def with def_timed:

def_timed foo([a | _], %{wat: b}) do
  ...
end

could expand to something like:

def foo(arg1, arg2) do
  Timer.wrap_timed(
    &untimed_foo/2,
    [arg1, arg2],
    :foo
  )
end

def untimed_foo([a | _], %{wat: b}) do
  ...
end

untimed_foo would use the original args AST unaltered, while the code generated in foo only cares about length(args).

Supporting default args directly in def_timed would be a lot of extra hassle, but using a do-less def would let you avoid that:

def timed_thing_with_defaults(x, y \\ 1, z \\ 2)
def_timed timed_thing_with_defaults(x, y, z) do
  ...
end

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews