How to set module variables in a module via macro?

I have this:

defmodule MyLib do
  defmacro __using__(options) do
    quote do
      # @var1 options[:var1]
      # @var2 options[:var2]

      @var1 unquote(options[:var1])
      @var2 unquote(options[:var2])

    end
  end


  def func1(_a) do
    IO.puts("var1 is: #{@var1}")
  end

  def func2(_a) do
    IO.puts("var2 is: #{@var2}")
  end
end

And this:


defmodule MyClient do
  use MyLib, [var1: 333, var2: 555]

  MyLib.func1(nil)
  MyLib.func2(nil)
end

The code prints out nil for both variables. It’s because I’ve gotten it vise versa. Namely, they get injected into MyClient instead. Correct?

My goal is to set the module variables in MyLib in order to be able to use them throughout MyLib module. How? Simply passing them to each function as arguments won’t be a good solution because my real code is more complicated. Neither will work nicely utilizing config values either.

Prior to use MyLib, ..., the MyLib module will be compiled and there’s no chance to inject anything into it. Additionally, two other considerations:

  1. Macros accept AST and return AST. They are functions and generally don’t have side effects.
  2. Macro callers don’t change the called context. If thats what you’re after it sounds a little “object oriented” and you’re seeking for some kind of class variable. Module attributes aren’t anything like that.

In summary:

  1. A macro, by definition, inserts code at the called site. So whatever MyLib.using/1 generates is inserted into the calling module, in this case MyClient.

  2. A macro can access the calling environment via the __CALLER__ special variable but thats not necessary here since you are already passing a binding ([var1: 333, ...])

  3. If you are seeking to customise the generated code based upon the parameters to __using__/1 then just extract them from option when you need them in the generated code as you are doing.

  4. The functions in MyLib cannot access the compile-time environment of MyClient - MyLib us compiled before the macro is called and all environment variables have gone so this path is not viable, or even really appropriate - state is not maintained in a module or a function.

  5. In general, the Elixir approach is to inject generated code into the client module and have it call a function in the MyLib module with the given parameter if required. For example something like this:

defmodule MyLib do
  defmacro __using__(options) do
    quote do
      def func1(),  do: MyLib.func1(unquote(options[:var1]))
      def func2(),  do: MyLib.func1(unquote(options[:var2]))
    end
  end

  def func1(var1) do
    IO.puts("var1 is: #{var1}")
  end

  def func2(var2) do
    IO.puts("var2 is: #{var2}")
  end
end

defmodule MyClient do
  use MyLib, [var1: 333, var2: 555]

  func1()
  func2()
end

(not tested)

3 Likes

Thx. But func1/0 and func2/0 aren’t visible in MyClient:


defmodule MyClient do
  use MyLib, [var1: 333, var2: 555]

 # each is undefined
  func1()   # MyLib.func1()
  func2()  # MyLib.func2()
end

My typo, should be:

defmodule MyClient do
  use MyLib, [var1: 333, var2: 555]

  def some_function do
    IO.inspect func1()
    IO.inspect func2()
  end
end
1 Like

That works, thx