How to pass Caller Module alias and imports to child macro

Currently, I want to organize context functions in separate files as it’s getting cluttered

The below 2 lines are getting repeated in all __USING__ macros - I don’t want to repeat these lines instead declare once in the parent module

import Ecto.Query, warn: false
alias Server.Repo

AS-IS (Working)

defmodule Server.Quiz do 
  use Server.Quiz.Access.Question 
  use Server.Quiz.Access.Choice
end

defmodule Server.Quiz.Access.Question do
  defmacro __using__(_) do
    quote do      
       import Ecto.Query, warn: false
       alias Server.Repo       

      alias Server.Quiz.{Question, Choice}

      def list_questions(resource) do
        Question
        |> where([q], q.tenant_id == ^resource.tenant.id)
        |> order_by(desc: :updated_at)
        |> Repo.all()
        |> Repo.preload([choices: (from c in Choice, order_by: c.seq)])
      end
   end
 end
end

TO-BE

defmodule Server.Quiz do 
  import Ecto.Query, warn: false
  alias Server.Repo       
  use Server.Quiz.Access.Question 
  use Server.Quiz.Access.Choice
end
defmodule Server.Quiz.Access.Question do
  defmacro __using__(_) do
    quote do     

      alias Server.Quiz.{Question, Choice}

      def list_questions(resource) do
        Question
        |> where([q], q.tenant_id == ^resource.tenant.id)
        |> order_by(desc: :updated_at)
        |> Repo.all()
        |> Repo.preload([choices: (from c in Choice, order_by: c.seq)])
      end
    end
  end
end

I don’t want to repeat below code in each refactor and just want once in module Server.Quiz

import Ecto.Query, warn: false
alias Server.Repo   

Formatting note: your examples would be significantly more readable with code-fences (leading and trailing ```) - especially for things like def __using__(_) that otherwise get interpreted as Markdown

What happens when you try the code listed under TO-BE in your post? I get the same results from putting the import in the using code as in the macro:

defmodule MacroThingy do
  defmacro __using__(_) do
    quote do
      import Ecto.Query, warn: false

      def inside_macro() do
        from(u in Core.User, where: u.id < 0)
      end
    end
  end
end

defmodule UserThingy do
  use MacroThingy

  def inside_user() do
    from(u in Core.User, where: u.id > 0)
  end
end

defmodule MacroThingy2 do
  defmacro __using__(_) do
    quote do
      def inside_macro() do
        from(u in Core.User, where: u.id < 0)
      end
    end
  end
end

defmodule UserThingy2 do
  import Ecto.Query, warn: false
  use MacroThingy2

  def inside_user() do
    from(u in Core.User, where: u.id > 0)
  end
end

iex(26)> UserThingy.inside_user()                 
#Ecto.Query<from u0 in Core.User, where: u0.id > 0>

iex(27)> UserThingy.inside_macro()
#Ecto.Query<from u0 in Core.User, where: u0.id < 0>

iex(28)> UserThingy2.inside_user()
#Ecto.Query<from u0 in Core.User, where: u0.id > 0>

iex(29)> UserThingy2.inside_macro()
#Ecto.Query<from u0 in Core.User, where: u0.id < 0>

I am getting Repo module not found as in macro I am just using Repo and not Server.Repo as I have mentioned it once in the parent module ( Server.Quiz)

My question is how can we use alias and imports of parent module in child 's __using__

You cant, that’s because of “macro hygiene”.

The idiomatic way is to pass the repo via an argument to the use/2 call.

2 Likes

This is a good goal, but I do not recommend doing it the way you are now. Macros essentially copy and paste code, and this makes things like stacktraces a lot worse. If you get an error with order_by for example, it will just point to the line where you do use Server.Quiz.Access.Question, it will not point to the order_by line.

Macros are not a code organizatino tool, they have other purposes. If you want to organize your code, make ordinary functions and defdelegate if you want some subset of those to show up on your root Quiz module. If you have hundreds of these, you probably want to find a way to narrow the scope of your public API.