Pattern Matching on Macros head

Hi, could someone explain to me what I’m doing wrong here. I’m trying to pattern match on a defmacro arguments.

Basically I have this:

defmacro div_with_content(content, body, socket, el_id, chain, el_contents) do
    quote do
      content_tag(:div, Phoenix.HTML.raw(
            "#{unquote(content)}#{(
              for {id, contents} <- unquote(body) do
                safe_to_string(live_render(unquote(socket), Cocktail.Element, session: %{section: :div, contents: contents, id: id, chain: [unquote(el_id) | unquote(chain)]}, child_id: id))
              end)}"
          ),
        [
          {:cocktail, 1},
          {:id, unquote(el_id)},
          {:"cocktail-chain", Jason.encode!(:lists.reverse(unquote(chain)))}
          | Map.get(unquote(el_contents), :attrs, [])
        ]
      )
    end
end

This one works fine. Now I was trying to refactor all the functions for divs into a single one with multiple macro definitions, the equivalent to this one would be (there are other 2 with the same name, different patterns):

defmacro cocktail_tag(
    %{
      tag: :div,
      body: [_|_] = body,
      content: content,
      attrs: attrs,
      style: style,
      classes: classes,
      interactions: _interactions
    },
    socket, el_id, chain) do
    
    quote do
      content_tag(:div, Phoenix.HTML.raw(
            "#{unquote(content)}#{(
              for {id, contents} <- unquote(body) do
                safe_to_string(live_render(unquote(socket), Cocktail.Element, session: %{section: :div, contents: contents, id: id, chain: [unquote(el_id) | unquote(chain)]}, child_id: id))
              end)}"
          ),
        add_attributes_list(el_id, chain, attrs, style, classes)
      )
    end
end

But when I try to use the last one, I get:

== Compilation error in file lib/cocktail_web/views/page_view.ex ==
** (FunctionClauseError) no function clause matching in CocktailWeb.LayoutView.cocktail_tag/4    
    (cocktail) expanding macro: CocktailWeb.LayoutView.cocktail_tag/4
    (cocktail) lib/cocktail_web/templates/page/editor_element.html.leex:6: CocktailWeb.PageView."editor_element.html"/1
    (phoenix) /Users/mnussbaumer/code/cocktail/lib/cocktail_web/views/page_view.ex:1: Phoenix.Template.__before_compile__/1
    (elixir) lib/kernel/parallel_compiler.ex:206: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6

So I’m wondering if it’s impossible to do this? What is wrong with my approach?

How did you call that macro? Remember that macros receive AST, not Elixir types, so if you call macro with:

cocktail_tag(%{tag: :div, …}, …)

Then your macro will receive:

{:%, env, [tag: :div]}

Instead of expected map. Another question is whether you need macro at all, because right now it doesn’t seem so and you should avoid macros as long as you can.

5 Likes

thanks hauleth!
I changed it to:

defmacro cocktail_tag(
    {:%, _env,
     [
       attrs: attrs,
       body: [_|_] = body,
       classes: classes,
       content: "",
       interactions: _interactions,
       tag: :div,
       style: style
     ]
    },
    socket, el_id, chain) do

But still get the same error. I imagine that because it’s a keyword list the order will matter?

In fact. I was just thinking that given that this is to render arbitrary nested “components”, with liveview that it would be a more performant approach to it (I’m basically toying with the idea of a live visual html editor, so there can and probably would be a lot of nested inside nested components).

Perhaps I should roll back to using plain functions? (I went from plain functions to individual macros and now trying to tidy up the macros)

Definitely just go with functions. You aren’t actually doing any work at compile time here anyway, so there isn’t really a performance gain other than removing 1 largely transparent function call.

5 Likes

Yes indeed, I think you’re both right and went back to plain functions (still it would be nice to know how usually one deals with that case)
thanks for the feedback!

Also macros are less flexible, as with code above you can do:

cocktail_tag(%{attrs: attrs, body: body, …}, …)

But if you do:

data = %{…}

cocktail_tag(data, …)

It will not work. And as @benwilson512 already said, it will not be faster, it will only make compilation longer and less flexible.

Ah - in the meanwhile I scrapped the whole macro thing (I usually only write them for Ecto queries because I have to, I was having ideas and thought they could fit here but will leave that for and if they ever become needed), so I can’t retry - but you’re basically saying that if I make the call with an explicit map, it would pattern match correctly the macro def, while if I pass a variable without explicitly stating the fields it won’t?

I still don’t see how the inner ast keyword list can match unless I mimic exactly the order of the keywords?
I can see moving the decision to inside the macro body, and then fetching the keywords, but then to me it just becomes much less legible - I’m always confused by macros so unless it really improves something significant I’ll keep away from them as suggested

I mean that in theory it will be possible to use AST in first case and match against map, but it will still not work in the way you expect it, so in the end - it will not work. What I am saying is that even if you would manage it to work, then in second case it would spectacularly fail anyway.

And yes, if you can not use macro, then do not use macro. It will result in better code and will be easier to maintain (and errors will have more sense in case of any).

1 Like