24

I'm pretty new to Elixir and functional programming-languages in general.

In Elixir, I want to call one specific function on Modules, given the Module name as String.

I've got the following (very bad) code working, which pretty much does what I want to:

module_name = elem(elem(Code.eval_file("module.ex", __DIR__), 0), 1)
apply(module_name, :helloWorld, [])

This (at least as I understand it) compiles the (already compiled) module of module.ex in the current directory. I'm extracting the modules name (not as a String, don't know what data-type it actually is) out of the two tuples and run the method helloWorld on it.

There are two problems with this code:

  1. It prints a warning like redefining module Balance. I certainly don't want that to happen in production.

  2. AFAIK this code compiles module.ex. But as module.ex is already compiled and loaded, it don't want that to happen.

I don't need to call methods on these modules by filename, the module-name would be ok too. But it does have to by dynamic, eg. entering "Book" at the command line should, after a check whether the module exists, call the function Book.helloWorld.

Thanks.

lschuermann
  • 862
  • 1
  • 7
  • 17

4 Answers4

28

Well, thats where asking helps: You'll figure it out yourself the minute you ask. ;)

Just using apply(String.to_existing_atom("Elixir.Module"), :helloWorld, []) now. (maybe the name "Module" isn't allowed, don't know)

lschuermann
  • 862
  • 1
  • 7
  • 17
  • 2
    Small note: you should use to_existing_atom whenever possible. Atoms arent garbage collected! – Patrick Oscity Apr 17 '16 at 21:11
  • Thanks! I didn't know that atoms aren't garbage collected. Will edit my answer. – lschuermann Apr 17 '16 at 21:13
  • More reason to use `String.to_existing_atom`: `String.to_atom` exposes an attack vector. See https://til.hashrocket.com/posts/gkwwfy9xvw-converting-strings-to-atoms-safely for details – biagidp Aug 19 '19 at 03:39
  • I tried the existing atom approach, but was getting an error saying that the atom did not exist. This was driving me crazy, because I had the module name (as a string) correct. Applying JasonG's solution worked. – CHsurfer Mar 03 '23 at 05:26
13

Note that you always need to prefix your modules with "Elixir."

defmodule Test do
  def test(text) do
    IO.puts("#{text}")
  end
end

apply(String.to_existing_atom("Elixir.Test"), :test, ["test"])

prints "test" and returns {:ok}

JasonG
  • 5,794
  • 4
  • 39
  • 67
5

Here is a simple explanation:

Assuming that you have a module like this:

defmodule MyNamespace.Printer do
  def print_hello(name) do
    IO.puts("Hello, #{name}!")
  end
end

Then you have a string that hold the module name that you can pass around in your application like this:

module_name = "Elixir.MyNamespace.Printer"

Whenever you receive the string module_name, you can instantiate a module and call functions on the module like this:

module = String.to_existing_atom(module_name)
module.print_hello("John")

It will print:

Hello, John!

Another way to dynamically call the function MyNamespace.Printer.print_hello/1 is:

print_hello_func = &module.print_hello/1
print_hello_func.("Jane")

It will print:

Hello, Jane!

Or if you want to have both module_name as atom and function_name as atom, and pass them around to be executed somewhere, you can write something like this:

a_module_name = :"Elixir.MyNamespace.Printer"
a_function_name = :print_hello

Then whenever you have a_module_name and a_function_name as atoms, you can call like this:

apply(a_module_name, a_function_name, ["Jackie"])

It will print

Hello, Jackie!

Of course you can pass module_name and function_name as strings and convert them to atoms later. Or you can pass module name as atom and function as a reference to function.

Châu Hồng Lĩnh
  • 1,986
  • 1
  • 20
  • 23
4

Also note that the name of a module is an atom so doing String.to_existing_atom isn't usually needed. Consider this code:

defmodule T do
  def first([]), do: nil
  def first([h|t]), do: h
end

In this case you can simply do the apply in this fashion:

apply(T,:first,[[1,2,3]])
#=> 1 

Or this example (List below is the Elixir List module):

apply(List,:first,[[1,2,3]]) 
#=> 1

I mean to say that if you know the name of the module, it's not necessary to pass it as a string and then convert the string to an existing atom. Simply use the name without quotation marks.

Onorio Catenacci
  • 14,928
  • 14
  • 81
  • 132
  • Yes, that is totally correct. But in my case, it was the goal to make the Module-Name dynamic. Of course, if you know the Module's name, that would work too. – lschuermann Apr 30 '16 at 13:46