Elixir - 通过字符串名称调用模块方法

24

我对Elixir和函数式编程语言都比较陌生。

在Elixir中,我想通过给定模块名称的字符串来调用一个特定的函数。

我已经写出了以下(非常糟糕的)代码,它基本上实现了我想要的功能:

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

这个(至少是我理解的)会在当前目录中编译已经编译好的module.ex模块。我从两个元组中提取模块名称(不是字符串,实际上我不知道它是什么数据类型),并对其运行方法helloWorld

这段代码有两个问题:

  1. 它会打印一个警告,例如redefining module Balance。我肯定不想在生产环境中发生这种情况。

  2. 据我所知,此代码将编译module.ex。但是,由于module.ex已经编译和加载,我不希望这种情况发生。

我不需要按文件名调用这些模块上的方法,模块名也可以。但它必须是动态的,例如,在命令行输入“Book”应该在检查模块是否存在后,调用函数Book.helloWorld

谢谢。

4个回答

28

好的,问问题是解决问题的关键:你一旦问出来就会自己找到答案。 ;)

现在只需使用apply(String.to_existing_atom("Elixir.Module"), :helloWorld, [])。(也许名称“Module”不允许,我不确定)


2
小提示:尽可能使用to_existing_atom。原子不会被垃圾回收! - Patrick Oscity
谢谢!我不知道原子不会被垃圾回收。我会修改我的答案。 - lschuermann
更多使用 String.to_existing_atom 的原因:String.to_atom 暴露了一个攻击漏洞。请参阅 https://til.hashrocket.com/posts/gkwwfy9xvw-converting-strings-to-atoms-safely 以获取详细信息。 - biagidp
我尝试了现有的原子方法,但是出现了一个错误,说该原子不存在。这让我很疯狂,因为我已经正确地输入了模块名称(作为字符串)。应用JasonG的解决方案成功了。 - CHsurfer

13

请注意,您始终需要在模块名称前加上 "Elixir." 前缀。

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

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

打印出 "test" 并返回 {:ok}


5

以下是简单说明:

假设你有这样一个模块:

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

然后你有一个保存模块名称的字符串,可以像这样在应用程序中传递:

module_name = "Elixir.MyNamespace.Printer"

无论何时收到字符串module_name,您都可以实例化一个模块并像这样调用模块上的函数:
module = String.to_existing_atom(module_name)
module.print_hello("John")

它会打印:

Hello, John!

动态调用函数MyNamespace.Printer.print_hello/1的另一种方法是:

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

它将打印出以下内容:
Hello, Jane!

如果你想将module_name和function_name都设置为原子(atom),并将它们传递到某个地方执行,可以按以下方式编写代码:

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

然后,当你有一个模块名称和一个函数名称作为原子时,你可以这样调用:

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

它将会打印

Hello, Jackie!

当然你可以将 `module_name` 和 `function_name` 作为字符串传递,稍后将它们转换为原子。或者你可以将模块名作为原子传递,并将函数的参考作为引用。

4

请注意,模块的名称是原子,因此通常不需要执行String.to_existing_atom。请考虑以下代码:

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

在这种情况下,您可以简单地按照以下方式进行应用:
apply(T,:first,[[1,2,3]])
#=> 1 

以下是Elixir List模块的示例:

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

我的意思是,如果你知道模块的名称,就不需要将它作为字符串传递并将字符串转换为现有的原子。只需使用没有引号的名称即可。


是的,那完全正确。但在我的情况下,目标是使模块名称动态化。当然,如果您知道模块的名称,那也可以。 - lschuermann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接