Creating a case with guards in a macro

Hi there!

I’m trying to write a macro that creates new code based on the macro parameters. What I would like to create is a case statement that uses guards in the cases, where the guard comparison value is generated from the input parameters.

So the code I would like to create is something like this, for example:

case x do
  x when x in [:foo, :bar] -> x
  _ -> :boom
end

The issue is the guard clause. I want that little list [:foo, :bar] to be dynamically created by a macro, something like:

defmacro incredible_code(my_map) do
  keys = Map.keys(my_map)
  
  quote do
    case :something do
      x when x in keys -> :ok
      _ -> :error 
    end
  end
end

I didn’t really bother with the unquotes in there, I know that code does not work :wink: I can get as far as getting the usual warning about the guard having to be a compile-time list. Which is all very fine. What I am trying to do is generate that list at compile time from that map, so theoretically, that warning should not apply in this case, as far as I understand macro’s. Because macro’s generate compile-time code. Right?

Does anybody have an idea?

Here are a couple of approaches. Neither of these are good recommendations but they are build, like your example, with the map outside the quote which means you need to convert the AST into a map again before you can use it.

  defmacro incredible_code(my_map) do
    # Option 1: Relies upon internal knowledge of the AST
    {:%{}, _, map} = my_map_as_keyword_list
    keys = Keyword.keys(my_map_as_keyword_list)

    # Option 2: Will execute arbitrary code so needs to be carefully managed
    {map, []} = Code.eval_quoted(my_map)
    keys = Map.keys(map)

    quote do
      case :something do
        x when x in unquote(keys) -> :ok
        _ -> :error
      end
    end
  end

A better approach would be the following using bind_quoted as Macro.escapeing the map (since the map encoding is not valid AST).

  defmacro incredible_code(my_map) do
    quote bind_quoted: [my_mapp: Macro.escape(my_map)] do
      case :something do
        x when x in unquote(Map.keys(my_map)) -> :ok
        _ -> :error
      end
    end
  end

(noting that this code also isn’t correct since its not unquoteing the other parameters as in your example).

1 Like

Thanks! It seems this works:

defmacro demo(my_map, x) do
  {:%{}, _, my_map_as_keyword_list} = my_map
  keys = Keyword.keys(my_map_as_keyword_list)

  quote do
    case unquote(x) do
      y when y in unquote(keys) -> :ok
      _ -> :error
    end
  end
end

Glad it works - but I’d suggest you use my last example as being more idiomatic and less error prone. For example, your code probably won’t work with string keys.