Call attr macro from another macro (noob question)

I am playing around with macros for the first time and I am feeling a little dumb that I don’t understand the following.

I am trying call the attr macro from within a custom macro with arguments of a map which is given to my macro.

The example is a little contrived, but I really can’t figure out how to wrap the existing attr macro.

defmacro my_attr(config) do
  quote bind_quoted: [config: config] do
    %{name: name} = config

    IO.inspect(name, label: "name")

    attr(name, :string, [])
  end
end

## in another module

my_attr(%{name: :hello})

The call to IO.inspect writes :hello to the console, but for the call to attr I get a No function clause matching error.

== Compilation error in file lib/pigeon_web/live/settings_live.ex ==
** (FunctionClauseError) no function clause matching in Phoenix.Component.attr/3    
    (phoenix_live_view 0.18.3) expanding macro: Phoenix.Component.attr/3
    lib/pigeon_web/live/settings_live.ex:25: PigeonWeb.SettingsLive (module)
    (pigeon 0.1.0) expanding macro: CVA.LiveView.my_attr/1
    lib/pigeon_web/live/settings_live.ex:25: PigeonWeb.SettingsLive (module)
```

attr expect an atom as first parameter, not a variable. Macros act on AST and in AST you cannot substitute values through variables unless both are actually supported. In your case you’d need to attr(unquote(name), :string, []) to make the AST received by the macro hold the atom :hello instead of the variable name.

I thought that calling unquote(name) would include the actual :hello atom, too. But doing so results in the same error.

Try adding |> tap(fn ast -> IO.inspect(Macro.to_string(ast)) end) after the quote do … end to see the generated code.

That’s the output.

config = %{name: :hello}\n\n(\n  %{name: name} = config\n  IO.inspect(name, label: \"name\")\n  attr(unquote(name), :string, [])\n)

What I also noticed: If I call attr with 2 arguments I can see the arguments passed in, which shows that attr received an AST with the unquote call. Don’t know if it helps.

== Compilation error in file lib/pigeon_web/live/settings_live.ex ==
** (FunctionClauseError) no function clause matching in Phoenix.Component."MACRO-attr"/4

    The following arguments were given to Phoenix.Component."MACRO-attr"/4:

        # 1
        #Macro.Env<aliases: [{JS, Phoenix.LiveView.JS}], context: nil, context_modules: [PigeonWeb.SettingsLive], file: "/Users/benvp/Development/pigeon/lib/pigeon_web/live/settings_live.ex", function: nil, functions: [{CVA.LiveView, [assign_cva: 3]}, {Phoenix.VerifiedRoutes, [static_integrity: 2, static_path: 2, static_url: 2, unverified_path: 3, unverified_path: 4, unverified_url: 2, unverified_url: 3]}, {PigeonWeb.ComponentHelpers, [slot?: 1]}, {PigeonWeb.Helpers, [merge_js: 2, platform_from_user_agent: 1, put_flash_unauthorized: 1]}, {PigeonWeb.Gettext, [handle_missing_bindings: 2, handle_missing_plural_translation: 7, handle_missing_translation: 5, lgettext: 4, lgettext: 5, lngettext: 6, lngettext: 7]}, {PigeonWeb.Components, [button: 1, confirm_modal: 1, hide_modal: 1, hide_modal: 2, icon_button: 1, link_button: 1, link_icon_button: 1, modal: 1, show_modal: 1, show_modal: 2]}, {PigeonWeb.CoreComponents, [error: 1, flash: 1, header: 1, hide: 1, hide: 2, input: 1, label: 1, show: 1, show: 2, simple_form: 1, translate_error: 1, translate_errors: 2]}, {Phoenix.HTML, [attributes_escape: 1, html_escape: 1, javascript_escape: 1, raw: 1, safe_to_string: 1]}, {Phoenix.Component, [assign: 2, assign: 3, assign_new: 3, assigns_to_attributes: 1, assigns_to_attributes: 2, changed?: 2, dynamic_tag: 1, focus_wrap: 1, form: 1, intersperse: 1, link: 1, live_component: 1, live_file_input: 1, live_flash: 2, live_img_preview: 1, live_render: 2, live_render: 3, live_title: 1, update: 3, upload_errors: 1, upload_errors: 2]}, {Kernel, [!=: 2, !==: 2, *: 2, **: 2, +: 1, +: 2, ++: 2, -: 1, -: 2, --: 2, /: 2, <: 2, <=: 2, ==: 2, ===: 2, =~: 2, >: 2, >=: 2, abs: 1, apply: 2, apply: 3, binary_part: 3, binary_slice: 2, binary_slice: 3, bit_size: 1, byte_size: 1, ceil: 1, div: 2, elem: 2, exit: 1, floor: 1, function_exported?: 3, ...]}, {Phoenix.LiveView.Helpers, [live_file_input: 2, live_img_preview: 2, live_patch: 1, live_patch: 2, live_redirect: 1, live_redirect: 2, live_title_tag: 1, live_title_tag: 2]}, {Phoenix.LiveView, [allow_upload: 3, attach_hook: 4, cancel_upload: 3, clear_flash: 1, clear_flash: 2, connected?: 1, consume_uploaded_entries: 3, consume_uploaded_entry: 3, detach_hook: 3, disallow_upload: 2, get_connect_info: 1, get_connect_info: 2, get_connect_params: 1, push_event: 3, push_navigate: 2, push_patch: 2, push_redirect: 2, put_flash: 3, redirect: 1, redirect: 2, send_update: 2, send_update: 3, send_update_after: 3, send_update_after: 4, static_changed?: 1, transport_pid: 1, uploaded_entries: 2]}], lexical_tracker: #PID<0.307.0>, line: 25, macro_aliases: [{JS, {{PigeonWeb.SettingsLive, 2}, Phoenix.LiveView.JS}}], macros: [{CVA.LiveView, [attr_cva: 1, my_attr: 1]}, {Phoenix.VerifiedRoutes, [path: 2, path: 3, sigil_p: 2, url: 1, url: 2, url: 3]}, {PigeonWeb.Gettext, [dgettext: 2, dgettext: 3, dgettext_noop: 2, dngettext: 4, dngettext: 5, dngettext_noop: 3, dpgettext: 3, dpgettext: 4, dpgettext_noop: 3, dpngettext: 5, dpngettext: 6, dpngettext_noop: 4, gettext: 1, gettext: 2, gettext_comment: 1, gettext_noop: 1, ngettext: 3, ngettext: 4, ngettext_noop: 2, pgettext: 2, pgettext: 3, pgettext_noop: 2, pngettext: 4, pngettext: 5, pngettext_noop: 3]}, {Phoenix.HTML, [sigil_E: 2, sigil_e: 2]}, {Phoenix.Component.Declarative, [def: 2, defp: 2]}, {Phoenix.Component, [attr: 2, attr: 3, embed_templates: 1, embed_templates: 2, render_slot: 1, render_slot: 2, sigil_H: 2, slot: 1, slot: 2, slot: 3]}, {Kernel, [!: 1, &&: 2, ..: 0, ..: 2, ..//: 3, <>: 2, @: 1, alias!: 1, and: 2, binding: 0, binding: 1, dbg: 0, dbg: 1, dbg: 2, def: 1, defdelegate: 2, defexception: 1, defguard: 1, defguardp: 1, defimpl: 2, defimpl: 3, defmacro: 1, defmacro: 2, defmacrop: 1, defmacrop: 2, defmodule: 2, defoverridable: 1, defp: 1, defprotocol: 2, defstruct: 1, destructure: 2, ...]}, {Phoenix.LiveView.Helpers, [live_component: 2, live_component: 3, render_block: 1, render_block: 2, sigil_L: 2]}, {Phoenix.LiveView, [on_mount: 1]}], module: PigeonWeb.SettingsLive, requires: [Application, CVA.LiveView, Kernel, Kernel.Typespec, Phoenix.Component, Phoenix.Component.Declarative, Phoenix.HTML, Phoenix.LiveView, Phoenix.LiveView.Helpers, Phoenix.Template, Phoenix.VerifiedRoutes, PigeonWeb, PigeonWeb.ComponentHelpers, PigeonWeb.Components, PigeonWeb.CoreComponents, PigeonWeb.Gettext, PigeonWeb.Helpers], ...>

        # 2
        {:unquote, [line: 25], [{:name, [line: 25, counter: {PigeonWeb.SettingsLive, 19}], CVA.LiveView}]}

        # 3
        :string

        # 4
        []

    (phoenix_live_view 0.18.3) lib/phoenix_component.ex:1667: Phoenix.Component."MACRO-attr"/4
    (phoenix_live_view 0.18.3) expanding macro: Phoenix.Component.attr/2
    lib/pigeon_web/live/settings_live.ex:25: PigeonWeb.SettingsLive (module)
    expanding macro: CVA.LiveView.my_attr/1
    lib/pigeon_web/live/settings_live.ex:25: PigeonWeb.SettingsLive (module)

That’s your issue here. I guess you shouldn’t use bind_quoted then, so the unquote is evaluated in the context of the quote do block and not the module body the resulting AST is put into.

Thanks for helping me here, but seems that this doesn’t work either :frowning: This yields the same no function match error as before.

 defmacro my_attr(config) do
    quote do
      %{name: name} = unquote(config)

      IO.inspect(name, label: "name")

      attr(name, :string, [])
    end
  end
defmacro my_attr(config) do
    quote do
      attr(unquote(config.name), :string, [])
    end
  end

Yeah. I tried that, too. … I’m really confused here, but it doesn’t seem to work as config is an AST, where you cannot use the dot syntax to access it’s value.

== Compilation error in file lib/pigeon_web/live/settings_live.ex ==
** (KeyError) key :name not found in: {:%{}, [line: 25], [name: :hello]}. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
    (pigeon 0.1.0) expanding macro: CVA.LiveView.my_attr/1
    lib/pigeon_web/live/settings_live.ex:25: PigeonWeb.SettingsLive (module)

Also tried this, but then we are back at the initial “no function clause matching” error.

defmacro my_attr(config) do
    quote do
      attr(unquote(config).name, :string, [])
    end
  end

I am clearly missing some understanding on how macros work.

You’re not missing something. Macros are “working with code” and especially quote do sometimes makes it seem easier than it is – hence me falling for it as well. Generally I’d suggest using plain values or keyword lists rather than maps as the inputs to macros. Lists and tuple-2 values represent themselves in AST - a.k.a. those values are the same in AST form. That makes them easier to work with in macros. For maps you could also use the keyword list within the tuple to pull out the :hello.

As for the unquote. You cannot do anything outside the unquote to “get to the value” of :hello, because that again will make attr receive the AST of the expression or again a variable, but not the AST for :hello.

1 Like

I managed to get it work by either accepting a keyword list directly or by extracting the keyword list from the AST in the argument.

defmacro my_attr(config) do
    {_, _, kw} = config

    quote do
      attr(unquote(Keyword.get(kw, :name)), :string, [])
    end
  end
  defmacro my_attr(config) when is_list(config) do
    quote do
      attr(unquote(Keyword.get(config, :name)), :string, [])
    end
  end

Thanks again for your help!