Generating function head with [head | tail] list matching

I’m trying to generate a function head that includes a head/tail-style list pattern match, like so:

def get([{:id, id} | opts]) do
  # function body
end

However, I’m running into lots and lots of brick walls. I think the root of the problem is that sometimes the function being defined has this pattern matching, but other times it doesn’t. So I’m trying to dynamically build the argument list, and then insert it into the quote block with unquote fragments, but nothing seems to work. In other words, if I was hardcoding the two different cases, I would use [unquote(id_tuple) | unquote(opts)] in one case and just unquote(opts) in the other (as arguments in the function def).

The most common error I get is unquote called outside quote; less common is matching failures on :elixir_expand.expand_list. If I don’t get either of those errors, then compilation will inevitably fail because the variables weren’t unquoted correctly and it tries to evaluate them as functions.

@jotakami welcome to the forum!

Any chance you can share some code? Happy to help but a bit hard to make any suggestions without some code (working or not).

2 Likes

Welcome to the forum!

As @kip mentioned, some more infos would be nice.

Maybe the following example can give you a hint.

defmodule Foo do
  [:id, :foo]
  |> Enum.each(fn k ->
    def do_inspect([{unquote(k), v} | t]) do
      IO.inspect(v, label: unquote(k))
      do_inspect(t)
    end
  end)

  def do_inspect([_|t]), do: do_inspect(t)
  def do_inspect([]), do: :ok
end
iex(1)> Foo.do_inspect([{:id, 1}, {:foo, 2}, {:bar, 3}])
id: 1
foo: 2
:ok
iex(2)> Foo.do_inspect(nil)
** (FunctionClauseError) no function clause matching in Foo.do_inspect/1

    The following arguments were given to Foo.do_inspect/1:

        # 1
        nil

    Attempted function clauses (showing 4 out of 4):

        def do_inspect([{:id, v} | t])
        def do_inspect([{:foo, v} | t])
        def do_inspect([_ | t])
        def do_inspect([])
1 Like

Thanks for the quick responses, here’s the actual quote block where I’m trying to do all this:

quote bind_quoted: binding() do
  def unquote(verb)(unquote(args)) do
    @client.unquote(@client_type)(unquote(pre) ++ __pre__(), __post__() ++ unquote(post))
    |> Tesla.unquote(verb)(unquote(path), unquote(opts))
  end
end

If path is a string literal, then args should just be the name opts, but if path includes (quoted) interpolation then I want to pull out the interpolated name and put it at the front of the argument list as a pattern match.

So if path is the quoted expression "foo/#{bar}" then I want the arguments to look like [{:bar, bar} | opts] so that in the function body, unquoting the original interpolation will actually inject the bar that was matched in the function head. Does that make sense?

I think what makes this challenging is that I’m trying to handle both cases in a single quote block, because injecting the desired AST for args seems to be where things are falling apart.

Okay, I can now compile calls to this macro, but only by writing two separate macro bodies (for the string literal vs. interpolated string cases).

For string literals:

defp request(verb, path, pre, post) when is_binary(path) do
  quote bind_quoted: binding() do
    def unquote(verb)(opts \\ []) do
      @client.unquote(@client_type)(unquote(pre) ++ __pre__(), __post__() ++ unquote(post))
      |> Tesla.unquote(verb)(unquote(path), opts)
    end
  end
end

and for strings with interpolation:

defp request(verb, path, pre, post) do
  {path, param} = path_param(path)

  quote bind_quoted: binding() do
    def unquote(verb)([unquote(param) | opts]) do
      @client.unquote(@client_type)(unquote(pre) ++ __pre__(), __post__() ++ unquote(post))
      |> Tesla.unquote(verb)(
        unquote(path),
        Keyword.update(
          opts,
          :opts,
          [path_params: unquote(param)],
          &Keyword.put(&1, :path_params, [unquote(param)])
        )
      )
    end
  end
end

where e.g. path_param(quote do: "foo/#{bar}") would output {"foo/:bar", {:bar, Macro.var(:bar, nil) |> Macro.escape()}} so that path is a string suitable for Tesla.Middleware.PathParams and param is a tuple that can be unquoted into a keyword list.

Anyways, there is clearly a bit of redundancy in these two macros so the perfectionist in me wants to factor out the common pieces. It also feels like this would deepen my understanding of macros and quoting…