I am just learning macros (just trying some experiements after working through the exercises in Chris McCord’s book on metaprogramming).
I was inspired by a recent work example where we have these extremely formulaic Ecto query functions in all of our contexts. I started to experiment with a simple macro for creating a multi-clause function when given a key and that works fine. I then extended it using the basic approach outlined in Kernel.SpecialForms — Elixir v1.16.0 section (as was pointed out in some other macro questions).
That works, and is this version:
defmacro order_by_fields(fields_with_names) do
quote bind_quoted: [fields_with_names: fields_with_names] do
import Ecto.Query, only: [order_by: 3]
Enum.each(fields_with_names, fn {field_name, function_name} ->
def unquote(function_name)(query, direction) do
order_by(query, [schema], [{^direction, field(schema, unquote(field_name))}])
end
def unquote(function_name)(query, binding, direction) do
order_by(query, [{^binding, schema}], [
{^direction, field(schema, unquote(field_name))}
])
end
end)
end
end
My final version I wanted to upgrade to was to allow for introspection of the schema to create the list of fields to generate these functions for. For example, so the interface could be something like order_by_fields(exclude: [:email, :inserted_at])
.
To demonstrate what I mean, I layered in this extra section that attempts to introspect the caller’s schema definition (as a starting point before working in my opts
). I am able to verify that I have access to the expected list (the computed fields_with_names
) in the outer quote
block as follows, but the entire innermost quote block does not run anymore:
defmacro order_by_fields(_opts) do
quote do
excluded_fields = ~w/
__struct__
__meta__
__schema__
__changeset__
/a
fields_with_names =
__MODULE__
|> Macro.struct!(__ENV__)
|> Map.keys()
|> Enum.reject(&(&1 in excluded_fields))
|> Enum.map(&{&1, String.to_atom("order_by_#{&1}")})
|> IO.inspect(label: "fields_with_names (called and verified content is as expected)")
quote bind_quoted: [fields_with_names: fields_with_names] do
import Ecto.Query, only: [order_by: 3]
IO.inspect(fields_with_names, label: "Outside Enum.each (not called)")
Enum.each(fields_with_names, fn {field_name, function_name} ->
IO.inspect(fields_with_names, label: "Inside Enum.each (not called)")
def unquote(function_name)(query, direction) do
order_by(query, [schema], [{^direction, field(schema, unquote(field_name))}])
end
def unquote(function_name)(query, binding, direction) do
order_by(query, [{^binding, schema}], [
{^direction, field(schema, unquote(field_name))}
])
end
end)
end
end
end
I’m clearly missing something about how to properly nest quote blocks. Any assistance would be greatly appreciated.