Macro to generate function with body containing an arg and var bound in outer scope?

macros
how-to-question

#1

Hi,

I am trying to define a macro with 3 arguments (name, var, body) which is to generate function name/1
a) with argument var, possibly arbitrary complex pattern-matching expression
b) besides var, other variables bound in the outer scope can be used in the body

Example:

defmodule Mac3Use do
  import Mac3
  for {g, v} <- [g1: 1, g2: 2] do 
    gen String.to_atom("#{g}_x"), a,           do: {:ok, 1, a}
    gen String.to_atom("#{g}_y"), %{y: y} = a, do: {:ok, y, a}
  # gen String.to_atom("#{g}_z"), a,           do: {:ok, v, a} # undefined function v/0
  end
end

Below the implementation satisfying a). With slight modification it can handle guards. Unfortunately it doesn’t handle b) - I get “undefined function v/0” CompileError.

defmodule Mac3 do
  defmacro gen(name, var \\ quote(do: _), do: block) do
    var = Macro.escape(var)
    block = Macro.escape(block)
    quote bind_quoted: [name: name, var: var, block: block] do
      def unquote(name)(unquote(var)) do
        unquote(block)
      end
    end
  end
end

I was trying to modify this code without luck. Inspect of binding() inside quote shows g and v variables from the example with proper values bound, but unquote(block) doesn’t want to consume v.

Just a moment ago I checked that

    @v  v
    gen String.to_atom("#{g}_z"), a, do: {:ok, @v, a}

works, but it’s rather a hack.

As you could notice, the macro resembles ExUnit.Case.test/3, and its implementation is simplified version of that from ExUnit.

Is there an easy solution to this?

Thank you,
Waldemar.


#2

The v in the :ok 3-tuple is scoped inside the gen macro, you need to unquote it:

gen String.to_atom("#{g}_z"), a,           do: {:ok, unquote(v), a}

And that ‘should’ work I think?


#3

unquote(v) results in “unquote called outside quote” :frowning:


#4

Ah it’s not working like it does in a def's body, probably some special casing there… >.<

I wonder if it is because you are Macro.escapeing the block, why are you doing that (and why escaping the var too)?


#5

Good question. Some sort of hacking. Sure, all macro arguments are already AST(s). Nevertheless without escaping e.g. var, it will choke on bind_quoted. Comment out escaping var and you’ll get “undefined function a/0” for the first function being generated that worked earlier.

During experiments I realized that bind_quoted is somehow opposite to escaping (hence the name). In this particular use case this second escape (first is implicit and forms macro arguments) seems to prevent “finding” a value in bind_quoted construct for what is to be a function argument - the variable, for which there is no value to bind, right?

All this hacking is not mine - see ExUnit.Case.test/3 for comparison.


#6

Well the main thing I’m curious about is what is the point of this macro, wouldn’t this be easier?

  for {g, v} <- [g1: 1, g2: 2] do 
    def unquote(String.to_atom("#{g}_x"))(a),           do: {:ok, 1, a}
    def unquote(String.to_atom("#{g}_y"))(%{y: y} = a), do: {:ok, y, a}
    def unquote(String.to_atom("#{g}_z"))(a),           do: {:ok, unquote(Macro.escape(v)), a} 
  end

I think this is the XY problem, I’m not sure what is trying to be accomplished here. ^.^;


#7

Let’s say I want compile time bindings (variables, not attributes) to be visible in the body of something like ExUnit.Case.test/3.


#8

As you can see in the code, they are unquoteing contents (your body) first before escapeing it. Also they escape using the unquote: true option.

Perhaps you should evaluate those ways?


#9

I tried this earlier with no desirable effect. I think the whole point behind this quote/unquote/escape is to assure that testcases return :ok (it’s mentioned in the comment above the macro’s head).

To unhide the context a bit - I’d want such a feature in macros I am building around common_test; In common_test you can put a set of testcases in one group, and to run such the set with different input configurations, you need to place that group into several intermediate groups and provide specific configuration for such intermediates.

Definining intermediate groups can be tedious, so looping with for/1 seems to be the best fit for the job. If I don’t find a way how to do that, the whole work won’t be lost - variable bindings from outer scope would be nice addition, though.

Nevertheless I think the problem seems interesting in general.


#10

I still think unquote: true is the option you actually want… You’ll have to write unquote(v) then, but at least it should work. And as far as I remember ExUnit, you have to unquote “external” names there as well…


#11

You are probably right… Quick check:

defmodule XXXTest do
  use ExUnit.Case
  ext = 1
  test "xxx", cfg do
    assert %{async: _} = cfg
    assert unquote(ext) == 2
  end
end

yields

  1) test xxx (XXXTest)
     test/xxx_test.exs:4
     Assertion with == failed
     code:  assert 1 == 2
     left:  1
     right: 2
     stacktrace:
       test/xxx_test.exs:6: (test)

I will play a little more tomorrow (now is close to midnight).
Thank you very much,
Waldemar.


#12

I think changing

    gen String.to_atom("#{g}_z"), a,           do: {:ok, v, a} # undefined function v/0

to

    gen String.to_atom("#{g}_z"), a,           do: {:ok, unquote(v), a} # should work fine this way

should work. As far as I can tell, you want to use a value of v that’s only available at compile time in the module, but as you have it, v and a are just regular variables declared in the body of a function.


#13

Do note, that only works if v is valid as an AST (which in this specific case a raw number is), otherwise you want unquote(Macro.escape(v)) instead, which will always safely put it in the AST (well unless it is a PID or Ref or so). :slight_smile: