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
)
Is there a better - and equally straight forward - way for me to achieve what I’m trying to do ?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #hex











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
rvirding
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.
MadsBoydMadsen
Thanks for your interest and attempt to help
Doing this for the above example:
quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.putsI get:
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} = _astThe
argsis 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
argsto 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
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
doblock:MadsBoydMadsen
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
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:
BTW you should look into
:timer.tc/1.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.
:timer.tc/{1,2,3}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(...) endblock.I do agree the use-case here is a bit strange but sometimes people do things just to experiment so I won’t judge
billylanchantin
Ah got it. Yeah agreed a closure would probably be easier to implement since you can just change
)
:def_timerto:defand wrap thedoblock. (I think there is still the edge case of theafterblock but no one uses those so who cares.garrison
Honestly I forgot
defeven 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
Timercall. Stack traces would be ugly, though.al2o3cr
The tricky part is that a function doesn’t even need to name its arguments if it pattern-matches on them, for instance:
Replacing
defwithdef_timed:could expand to something like:
untimed_foowould use the originalargsAST unaltered, while the code generated infooonly cares aboutlength(args).Supporting default args directly in
def_timedwould be a lot of extra hassle, but using ado-lessdefwould let you avoid that: