I have seen macros being used in a lot of code at different parts but never really understood the thought process behind it. Can someone please list some examples with some thought process behind using macro ?
Can you add details to your question, are you interested in how macros work or what are the main use-cases?
Please give details on both
a good entry point is here Macros - The Elixir programming language, but also point 1 and 3 of the meta programming section
First thing to mention is when NOT to use a macro, that is, almost always. You only want to reach for a macro when you have no choice but to use one. See Library Guidelines — Elixir v1.15.5
There are several reasons you might need a macro, but almost all of them are for libraries, not your typical application code:
-
you need to do something at compile time, e.g. defining a function (
defn
inNx
,Plug.Router
…), which regular functions cannot do -
you want a specific DSL like
Ecto.Query
-
you want a syntax to generate a data structure that also works in patterns/guards (
../2
for ranges, Date/Time sigils like~D
…). You can doa..b = 1..10
, but functions cannot be called in patterns/guards. -
you need to access the AST, e.g.
assert/1
: if it was a function, it would only see the final value (false
), not the operation you’re trying to do (>
,==
…), the operands (left and right), or the code it is printing (code: assert 1 + 2 + 3 + 4 > 15
). -
performance/optimization:
Logger
could typically be implemented with functions, but we want to only evaluate the message if the logger level is above the configured level, and an API likeLogger.maybe_info(fn -> "hello #{world}" end)
would have a bigger runtime footprint (plus a more clunky API). Instead, we generate efficient code by checking the log level at compile time.
For an example the if/2
in Elixir is a macro that expands to a case/2
. You can see this if you disassemble the code from the BEAM file.
# macro.exs
Mix.install([{:beam_file, "~> 0.5.3"}])
defmodule Foo do
def pos(x) do
if x > 0 do
"yes"
else
"no"
end
end
end
|> BeamFile.elixir_code!()
|> IO.iodata_to_binary()
|> IO.puts()
> elixir macro.exs
defmodule Elixir.Foo do
def pos(x) do
case :erlang.>(x, 0) do
false -> "no"
true -> "yes"
end
end
end
I wouldn’t really know where to draw the line. If I want to abstract away some functionality in my codebase, but not produce a library, does that count? In that case, macros should be useful for building abstractions.
Abstractions should be modeled with modules and functions though.
Macros don’t help with abstraction. Macros cannot do what you cannot manually write out as well - by definition. So macros are always optional. They do however allow you to write less code manually by using code generation to let the computer create the boilerplate portion of the desired code.
That doesn’t mean you cannot use macros in your own codebase. But I also wouldn’t call them “typical application code”. Besides code needing to be a macro to interact with a macro of a library (e.g. ecto fragments, stuff around heex, stuff around verified routes, …) truely custom to an application macros in my experience are rare.
Not necessarily. Modeling things in terms of offloading business validations and having custom rules at compile-time is extremely powerful and can make code that is complex from business side into a delight to work on by application developers.
Not sure I agree with that. I’ve created in quite a few projects DSLs that subsequent developers would use successfully to write business logic in an easy manner. I hope to show more examples in the future as this is a topic that comes around from 90s and it is a very powerful concept, but it can be used only in languages that support metaprogramming.
I would say that the biggest problem now with writing a lot of macros and using metaprogramming is how wild this field is at the moment, it’s extremely powerful but at the same time it requires a better abstraction for specific things. I think that libraries like Spark are a great step into the right direction on abstracting and making easier to use the power of metaprogramming without going into its complexity, but still require a lot of experimenting.
I guess that’s down to what level of abstraction one is talking about. If you consider the plain difference in syntax the abstraction, then maybe.
I was talking about abstraction from the context of business logic, where macros don’t play a role. Like I could write @condition {:*, {:a, [], []}, {:b, [], []}}
or I could write condition a * b
. The interesting part here is not in the macro transforming the latter call to the former, but in the fact that somewhere there’s some validation system sitting making use of that defined condition.
On this I completely agree, this is why there is a fat warning to not use macros when functions can be used to achieve the same thing.
I would even add that this rule can be very blurry at times, as metaprogramming is an integral part of a lot of functionality of elixir. For example I’ve recently worked in a horrible codebase where someone thought it was a great idea to write code like this:
def abstraction() do
{ImplementationModule, :implmentation_function_name, fn ->
# throw in here some business logic
end
}
end
# Then they executed the thing above by
{module, func_name, arg} = abstraction()
apply(module, func_name, [arg])
This not only made the project impossible to navigate, but also started to surface some idiotic runtime errors that could have been avoided in the first place by writing the code in the most simple way.
Yes, and even with macros this can be followed by doing essentially this:
defmacro mymacro(ast) do
data_from_ast = …
computed = MyMacroModule.BusinessLogic.function(data_from_ast)
ast_from_computed = …
ast_from_computed
end
The macro essentially become the code transforming layer around logic defined in functions, which deal with data.
That is nasty .
I was always complaining before that the metaprogramming is poorly documented in elixir, but this seems to be a blessing in disguise. I can only imagine the possible horrors of a codebase that was written by someone that has no idea how to write elixir and decided to use heavily metaprogramming.
I think the general advice of not using macros at all is a good one. While you can gain some benefits, the potential dangers and downsides if you don’t know what you are doing are much greater and don’t make sense for 90% of cases.
This wasn’t meant as an alternative to functions, but as actual advice for the cases where macros are indeed useful. E.g. the Ecto.Query
codebase if full of that pattern and I’ve seen it used in many other places as well.
Usefulness cannot be denied, but at the same time this mix of code/data processing mixed with AST modification/generation is one of the reasons why metaprogramming can become a problem rather than a solution really fast.
IMO this problem could be potentially solved by having right abstractions in place, but once again this topic is extremely complex. I’ve written quite a few macros and I still sometimes don’t understand some of the concepts and limitations they impose, it’s a huge rabbit-hole.
I have to disagree, I’ll give you an example:
I have tables with fields like name_uk, name_en, name_ru - etc. I don’t plan to support too many locales (or dynamically add them), that’s why I decided to do localization this way.
Now I want to generate a schema (a module) per locale per table. Imagine, how much duplication I would end up with, if not for macros.
With this I do agree, but my motivation not to use macros is simply because they are hard to write and reason about, not that I don’t like the end result, and such wouldn’t wish to use them more.
This may come down to the idea that the one that writes the macro always sees value in it but the ones that read and use it may not.
YMMV, but macros usually belong to tooling (Ecto, Phoenix, etc) and those usually can (most of the time should/could) be open source. If there is a push for some tooling/special handling in the code at a company, maybe push for it to be a library that can be open sourced and used in the project as a dependency. By making it open source, you will quickly see if any business logic (abstraction error) is creeping in there. As other folks pointed out, that’s usually the pain, getting the abstraction level right, not the writing the code itself.
If open source and it truly adds value, it will add value in different places as well. Folks in the community may say: Oh YES, that’s neat and helps me. You get lib downloads, maybe someone writes a blog about it, folks discuss it in the community, etc. If it doesn’t, it will be apparent (no lib downloads, no interest in contributions, etc) and will be solved without friction/unnecessary discussion of what may/may not add value in the long run.
Just my anecdotal experience :), YMMV!
Code generation is not abstraction. Abstractions generalize a concept, macros are essentially the same as if you’d written all those schemas yourself (hence generation).
Along the lines of what @pdgonzalez872, your particular example is what I would consider library, not business, logic. Going full open source with it is certain and interesting and probably even a very good idea, but it’s also totally cool to have custom proprietary libraries within your application. I like to put these at the top level. If I don’t have a cute name for them, I prefix it with my app’s name, but being at the top level helps keep that boundary.
For example, my more boring Phoenix projects often look like:
MyApp
MyAppWeb
MyAppUtil
MyAppUtil
just includes stuff that I feel is “missing” from stdlib and contains no business logic.
All that to say I think your use-case is a good one, but it would be preferable (IMO) to write it as a configurable library, even if you don’t open source, just to help keep the boundary. Otherwise, if business logic creeps in you’re going to start having a BadTime.
Can you elaborate on this? I might have used a wrong word, I didn’t mean code generation in the sense of what mix phx.gen.schema
does. It’s modules that get defined dynamically, something along these lines (note defmodule
):
defmacro __using__(_) do
# ...
known_locales = Gettext.known_locales(MyAppWeb.Gettext)
quote do
Enum.each(unquote(known_locales), fn locale ->
# ... (set `module`)
defmodule module do
# ...
end
end)
end
end