PatrickSachs

PatrickSachs

Accessing a module in the main app from a dependency

Hello there,

I have the following scenario:

I have a library my_lib that is used in my_app. my_lib has a configuration file option that accepts a list of modules. Based on the list of modules provided, a Plug is generated.

The config in my_app is defined like this:

config :my_lib,
  modules: [MyApp.Plugs.Plug1, MyApp.Plugs.Plug2]

The code in my_lib looks like this:

modules = Application.get_env(:my_lib, :modules)
for module <- modules do
  quote do
    forward "/module/#{unquote module}", to: unquote(module)
  end
end

This compiles, but a request to /module/Elixir.MyApp.Plugs.Plug1 just yields a 404 error. Interestingly, I also get a warning in VS Code in the for line, that modules cannot be iterated since it is nil, which to my understanding cannot be the case since I defined a default value of [] for it the mix.exs file of my_lib (I can also certainly confirm that it is not nil, since logging it prints the expected value).

def application do
  [
    mod: {MyLib.Application, []},
    env: [modules: []]
  ]
end

At this point I assumed that I either made an error in my meta-programming or since my_lib is compiled before my_app, that its modules are simply not yet available.
According to the documentation Code.ensure_compiled/1 would be a good candidate to wait for the compilation of these modules. So I added a call to this function and printed out its results, which were quite baffling to me:

modules = Application.get_env(:my_lib, :modules)
for module <- modules do
  compiled = Code.ensure_compiled(module)
  IO.puts("ensure_compiled #{inspect module} -> #{inspect compiled}")
  # = ensure_compiled MyApp.Plugs.Plug1 -> {:error, :nofile}
  # = ensure_compiled MyApp.Plugs.Plug2 -> {:error, :nofile}
  quote do
    forward "/module/#{unquote module}", to: unquote(module)
  end
end

Apparently these modules do not exist at all. However, calling this function directly in IEx, the call resolves successfully:

iex(1)> Code.ensure_loaded MyApp.Plugs.Plug1
{:module, MyApp.Plugs.Plug1}

If I had to take another guess the module resolution fails since my_lib is looking for a source file in its own lib folder and does not take other applications (my_app) into account.

  • Is there any way to use modules of the “parent” app during compilation? If so, how would I go about that?
  • What did I do wrong that Plug did not throw an error or return a 500, but just silently ignore my module (well, or just works for that matter :smiley: )?
  • Did I set the default config value incorrectly? It seems to work when executing, but since VS Code (using the elixir-ls extension) complains about it, I guess there must be something not quite right with it.

Thank you for your time! I realize that this is quite the wall of text :smiley:

Marked As Solved

kip

kip

ex_cldr Core Team

Since macros are primarily about manipulating code, not executing it, I’m not sure how your expectation could be met.

Nevertheless, you can evaluate AST at compile time with Code.eval_quoted/3 although as the documentation says:

Warning: Calling this function inside a macro is considered bad practice as
it will attempt to evaluate runtime values at compile time. Macro arguments are
typically transformed by unquoting them into the returned quoted expressions
(instead of evaluated).

Using your example:

defmacro __using__(opts) do  
  options = Code.eval_quoted(opts)
  modules = Keyword.get(options, :modules)

  quote do
    def get_modules(), do: unquote(modules)
  end
end

The benefit here isn’t really enough to warrant using Code.eval_quoted/3 so perhaps reverting closer to your original idea may be clearer since it too will get resolved at compile time (but in a later compiler expansion):

defmacro __using__(opts) do
  quote do
    modules = unquote(opts[:modules])
    @modules Enum.map(modules, &R.normalize_module/1)

    def get_modules(), do: @modules
  end
end

Also Liked

kip

kip

ex_cldr Core Team

Remember that everything in the quote block is actually runtime execution. Outside the quote is compile time execution. Therefore you are probably after:

defmacro __using__(opts) do  
  modules = 
    opts
    |> Keyword.get(:modules)
    |> Enum.map(&MyLib.normalize_module/1)

  quote do
    def get_modules(), do: unquote(modules)
  end
end

Do note however that macros both receive and return AST. So opts[:modules] may not be what you expect since it will be AST.

You may find that you need the following signature for MyLib.normalize_module/1:

def normalize_module({:__aliases__, _meta, [module_name]}) do
  module_name
end

or alternatively:

def normalize_module(module_ast) do
  Macro.expand(module_ast, __ENV__)
end
al2o3cr

al2o3cr

I don’t think the code being compiled in my_lib can see the code in my_app at all - it would massively complicate dependency tracking if files in the application could cause libraries to recompile.

Have you considered an approach like Ecto.Repo where callers say use MyLib, modules: [...etc...] in a module in my_app? The for loop and friends stay in my_lib but the use of them happens when my_app is compiled.

kip

kip

ex_cldr Core Team

Changing conn to var!(conn) will compile cleanly I believe.

PatrickSachs

PatrickSachs

I see, for some reason I always looked at them as a way of executing code at compile time, but this makes a lot more sense.

True, I never even considered the fact that a user might want to e.g. use a runtime configuration value. Then I guess I’ll budge and do the data transformation fully at runtime.

defmacro __using__(opts) do
   modules = opts[:modules]

   quote do
    def get_modules() do 
      unquote(modules)
      |> Enum.map(modules, &R.normalize_module/1)
    end
  end
end

I initially wanted to avoid this to save performance at runtime, but this doesn’t seem to be a good way to do it. Maybe this is a case of premature optimization being evil again :slight_smile:

Thanks for your time and patience. I wish I could mark the answers of multiple people as solution, but I’ll just go ahead and mark your last one.

Have a great weekend!

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New

Other popular topics Top

sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

We're in Beta

About us Mission Statement