Macro difficulties

Hi, I am trying to build a little DSL to query a database. There are several steps.

Step 1. Write macros that yield composable queries, e.g., QP.title("Foo") and QP.sort("title") . Thus

Document |> QP.title("Foo") |> QP.title("Bar") |> QP.sort("title") |> Repo.all

finds all documents with “Foo” and “Bar” in the title and presents them in sorted order.
I’ve been able to do this.

Step 2. This is where I am stuck. I’d like to take a pair like ["title", "Magick"] and turn it into the macro call QP.title("Magick"), so that, for example, I can do

docs =  Document |> QP.command(["title", "Magick"]) |> Repo.all

This fails, although following related code succeeds:

docs = Document |> QP.title("Magick") |> Repo.all

The error message is

** (CompileError) apps/koko/test/koko/doc_manager/qp_test.exs:23: invalid call "QP.title"(Document, "Magick")
    (koko) expanding macro: Koko.DocManager.QP.command/2
    test/koko/doc_manager/qp_test.exs:23: Koko.DocManagerTest."test command macro"/1
    ETC.

Thus it looks like "QP.title" is not evaluated, despite the fact that I have unquoted it. Here, finally, is the code for QP.command:

 defmacro command(query, pair) do
     [cmd, arg] = pair
     quote do
        unquote("QP.#{cmd}")(unquote(query), unquote(arg))
     end
   end

Steps 3 & 4. TBC

Added note (1) Maybe there is some kind of expand_macro that has to be applied to the string "QP.title"??

Update Using @marcuslankenau’s suggestion, I’ve modified things a bit:

   defmacro command(query, pair) do
     [cmd, arg] = pair
     cmd = "QP.#{cmd}"
     quote do
        apply(unquote(cmd), unquote(query), unquote(arg))
     end
   end

but get this nicer error message

 ** (ArgumentError) argument error
     stacktrace:
       :erlang.apply("QP.title", Koko.DocManager.Document, "Magick")
       test/koko/doc_manager/qp_test.exs:23: (test)

But "QP.title" is still unevaluated.

Added Note (2) Well, I am trying to unquote a string, so that won’t work. Here is another approach which almost works. I have a plain old function by_command which accepts a list of pairs [cmd, arg] and dispatches a call to the appropriate macro, QP.query1(commands), QP.query2(commands), etc., depending on the number of arguments. (Bad form, but it will do for the moment). I’ve tested the QP.query*(commands) macros, which construct a concatenation of queries based on the list of pairs. See (Code) below. Unfortunately, I get this error when I run a simple test:

** (ArgumentError) argument error
    :erlang.hd({:commands, [line: 15], nil})
    expanding macro: Koko.DocManager.QP.query1/1
    lib/koko/doc_manager/search.ex:15: Koko.DocManager.Search.by_command_list/1

Note that line 15 is indicated below.

Code

  def by_command_list(commands) do
    n = length commands
    case n do
      1 ->
        QP.query1(commands)  # LINE 15
      2 ->
        QP.query2(commands)
      ETC.
    end

I have no solution, but a few hints:

  1. try something like apply(QP, unquote(cmd), [unquote(query), unquote(arg)]
  2. accessing pair as an array is kind of dangerous. An macro receives syntax trees. Sometimes these syntax trees are the same as the literal. Try stuff in iex like:
quote do [1, 2] end

and then

quote do [1, 2+3] end

Maybe this talk is interesting for you: https://www.youtube.com/watch?v=IkZbtzLbfNU&t=62s

1 Like

I’ll try. Thanks!!