How to call apply/3 dynamically (with Strings)

I am setting up a function that I want to call using strings from a database.

Once I get the data from the database, it looks like this:

q = %Question{ choices_module: "Survey", choices_function: "get_choices" }

I have a function in Survey that looks something like this:

  def get_choices(question) do
   apply(q.choices_module, q.choices_function, [q])
  end

This does not work, and I get an error that I need to send an Atom as the module name. I tried something like:

String.to_atom(q.choices_module) # => :Survey

Which is not what it expects.

Working it out in the console, I see that it wants this:

apply(Survey, :get_choices_for_question, [q]) 

and this call works in the console.

I think I am fundamentally not understanding how to turn "Survey" to Survey.

Any ideas? Thanks in advance.

In elixir module names are prefixed with Elixir. This means when you see ModuleName in iex they are really Elixir.ModuleName

iex> Atom.to_string(String)
"Elixir.String"

So if you want to create a “module” from a string you need to prefix it with “Elixir.” first.

Try:

apply(String.to_existing_atom("Elixir." <> q.mod), String.to_existing_atom(q.fun), [Atom])
"Elixir.Atom"

You might want to check that the module is not prefixed with “Elixir.” already.

2 Likes

It’s better and safer to use Module.concat/1 in such a case rather than manually get into the implementation details.

["String"] |> Module.concat() |> apply(:length, ["foo"])
#⇒ 3
8 Likes

Thanks! This is exactly what I needed… With a slight twist. I am writing this function inside the Elixir.Survey module, so I was getting (module Survey is not available).

I changed the whole thing to __MODULE__ and everything works as expected.

BUT! I will need to do something similar across modules, so this knowledge will come in handy.

Thanks!