lakret
Getting variable by its dynamic atom name from the caller context in a macro
I’m struggling with overriding hygiene during manual manipulation of AST with Macro.postwalk. In short, I want to provide a syntax for interpolating variables with ^ in a DSL, similar to Ecto. So this:
some_col_name = "foo"
...
select [:x, ^some_col_name]
should be converted internally to
%Select{
columns: ["x", "foo"]
}
There is more logic around that, though, so I need to first replace these ^some_col_name in the AST with the values of corresponding variables, and then do some more manipulation with the AST.
My attempts so far were variations on a theme:
Macro.postwalk(quoted, fn
{:^, [], [{v, [], _}]} when is_atom(v) ->
quote do
var!(unquote(v))
end
x -> x
end)
I know that I need to override hygiene, but it seems var! only works with literal variable names, and there’s no example of using dynamic atom names with it that I could find. I also tried different combinations of Macro.var, unquote, and var!, to no avail.
Any suggestions?
Marked As Solved
al2o3cr
The REPL is obscuring things somewhat - interpolate runs during compilation and creates an AST which is then evaluated. bar has a value during the second part of that process.
Here’s a revised version that handles the example from your post:
defmodule Foo do
defmodule Column do
defstruct [:name]
end
defmacro select(columns) do
interpolate(columns)
end
def interpolate(q) do
IO.inspect(q, label: :q)
ast = Macro.postwalk(q, fn
{:^, _, [{name, _, _} = v]} when is_atom(name) ->
quote do
atom_to_column(var!(unquote(v)))
end
a when is_atom(a) ->
quote do
atom_to_column(unquote(a))
end
x -> x
end)
IO.inspect(ast, label: :ast)
ast
end
def atom_to_column(a) when is_atom(a) do
%Column{name: a}
end
end
Running this produces output like:
iex(32)> select([:foo, :bar])
q: [:foo, :bar]
ast: [
{:atom_to_column, [context: Foo, import: Foo], [:foo]},
{:atom_to_column, [context: Foo, import: Foo], [:bar]}
]
[%Foo.Column{name: :foo}, %Foo.Column{name: :bar}]
iex(33)> wat = :foobar
:foobar
iex(34)> select([:foo, ^wat])
q: [:foo, {:^, [line: 34], [{:wat, [line: 34], nil}]}]
ast: [
{:atom_to_column, [context: Foo, import: Foo], [:foo]},
{:atom_to_column, [context: Foo, import: Foo],
[{:var!, [context: Foo, import: Kernel], [{:wat, [line: 34], nil}]}]}
]
[%Foo.Column{name: :foo}, %Foo.Column{name: :foobar}]
Also Liked
al2o3cr
var! is a macro, so code like this:
var!(some_variable_name)
calls var! with {:some_variable_name, metadata, context}.
I found this example helpful:
defmodule MacroExample do
defmacro get_value(variable) do
IO.inspect(variable)
quote do
var!(unquote(variable))
end
end
end
iex(25)> require MacroExample
MacroExample
iex(26)> target_var = 42
42
iex(27)> MacroExample.get_value(target_var)
{:target_var, [line: 27], nil}
42
In your dynamic case, you likely want to capture the whole tuple and unquote it:
Macro.postwalk(quoted, fn
{:^, [], [{v, _, _} = v_tuple]} when is_atom(v) ->
quote do
var!(unquote(v_tuple))
end
x -> x
end)
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








