查看源代码 别名、require 和 import

为了方便软件重用,Elixir 提供了三个指令(aliasrequireimport)以及一个名为 use 的宏,总结如下

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Require the module in order to use its macros
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

我们现在将详细探讨它们。请记住,前三个被称为指令是因为它们具有词法作用域,而 use 是一个通用的扩展点,允许被使用的模块注入代码。

alias

alias 允许您为任何给定的模块名称设置别名。

想象一个模块使用在 Math.List 中实现的专用列表。该 alias 指令允许在模块定义中将 Math.List 称为 List

defmodule Stats do
  alias Math.List, as: List
  # In the remaining module definition List expands to Math.List.
end

原始的 List 仍然可以通过完全限定名称 Elixir.ListStats 中访问。

在 Elixir 中定义的所有模块都在主 Elixir 命名空间中定义,例如 Elixir.String。但是,为了方便起见,您可以省略“Elixir.”来引用它们。

别名经常用于定义快捷方式。事实上,在没有 :as 选项的情况下调用 alias 会自动将别名设置为模块名称的最后一部分,例如

alias Math.List

与以下代码相同

alias Math.List, as: List

请注意,alias词法作用域的,这使您能够在特定函数内设置别名

defmodule Math do
  def plus(a, b) do
    alias Math.List
    # ...
  end

  def minus(a, b) do
    # ...
  end
end

在上面的示例中,由于我们在函数 plus/2 中调用 alias,因此该别名仅在函数 plus/2 中有效。 minus/2 根本不会受到影响。

require

Elixir 提供宏作为元编程(编写生成代码的代码)机制。宏在编译时展开。

模块中的公共函数是全局可用的,但为了使用宏,您需要通过需要定义它们的模块来选择加入。

iex> Integer.is_odd(3)
** (UndefinedFunctionError) function Integer.is_odd/1 is undefined or private. However, there is a macro with the same name and arity. Be sure to require Integer if you intend to invoke this macro
    (elixir) Integer.is_odd(3)
iex> require Integer
Integer
iex> Integer.is_odd(3)
true

在 Elixir 中,Integer.is_odd/1 被定义为宏,以便可以用作保护。这意味着,为了调用 Integer.is_odd/1,我们需要先需要 Integer 模块。

请注意,与 alias 指令一样,require 也是词法作用域的。我们将在后面的章节中更多地讨论宏。

import

每当我们想要在不使用完全限定名称的情况下访问来自其他模块的函数或宏时,我们都会使用 import。请注意,我们只能导入公共函数,因为私有函数永远无法从外部访问。

例如,如果我们想要多次使用 List 模块中的 duplicate/2 函数,我们可以将其导入

iex> import List, only: [duplicate: 2]
List
iex> duplicate(:ok, 3)
[:ok, :ok, :ok]

我们只从 List 导入了 duplicate 函数(带 2 元)。虽然 :only 是可选的,但建议使用它,以避免将给定模块的所有函数导入到当前范围内。也可以使用 :except 作为选项,以便导入模块中的所有内容,除了函数列表之外。

请注意,import 也是词法作用域的。这意味着我们可以在函数定义中导入特定宏或函数

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    duplicate(:ok, 10)
  end
end

在上面的示例中,导入的 List.duplicate/2 仅在该特定函数中可见。 duplicate/2 在该模块中的任何其他函数(或任何其他模块)中都不可用。

虽然 import 可能对框架和库构建抽象很有用,但开发人员通常应该在自己的代码库中更喜欢 alias 而不是 import,因为别名使被调用的函数的来源更清晰。

use

use 宏经常用作扩展点。这意味着,当您 use 模块 FooBar 时,您允许该模块在当前模块中注入任何代码,例如导入自身或其他模块、定义新函数、设置模块状态等。

例如,为了使用 ExUnit 框架编写测试,开发人员应该使用 ExUnit.Case 模块

defmodule AssertionTest do
  use ExUnit.Case, async: true

  test "always pass" do
    assert true
  end
end

在幕后,use 需要给定的模块,然后在它上面调用 __using__/1 回调,允许模块将一些代码注入到当前上下文中。一些模块(例如,上面的 ExUnit.Case,还有 SupervisorGenServer)使用这种机制来为您的模块填充一些基本行为,您的模块应该覆盖或完成这些行为。

一般来说,以下模块

defmodule Example do
  use Feature, option: :value
end

编译为

defmodule Example do
  require Feature
  Feature.__using__(option: :value)
end

由于 use 允许任何代码运行,因此我们无法真正知道使用模块的副作用,除非阅读其文档。因此,请谨慎使用此函数,仅在严格需要时才使用。不要在 importalias 可以做到的地方使用 use

理解别名

此时,您可能想知道:Elixir 别名究竟是什么,它是如何表示的?

Elixir 中的别名是一个大写标识符(如 StringKeyword 等),在编译时被转换为原子。例如,String 别名默认情况下转换为原子 :"Elixir.String"

iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true

通过使用 alias/2 指令,我们正在更改别名扩展到的原子。

别名扩展为原子,因为在 Erlang 虚拟机(以及因此的 Elixir)中,模块始终由原子表示

iex> List.flatten([1, [2], 3])
[1, 2, 3]
iex> :"Elixir.List".flatten([1, [2], 3])
[1, 2, 3]

这就是我们用于调用 Erlang 模块的机制

iex> :lists.flatten([1, [2], 3])
[1, 2, 3]

模块嵌套

既然我们已经讨论了别名,现在我们可以谈论嵌套以及它在 Elixir 中是如何工作的。考虑以下示例

defmodule Foo do
  defmodule Bar do
  end
end

上面的示例将定义两个模块:FooFoo.Bar。只要它们在相同的词法作用域中,第二个模块可以在 Foo 中访问为 Bar

如果稍后将 Bar 模块移出 Foo 模块定义,则必须通过其完整名称 (Foo.Bar) 引用它,或者必须使用上面讨论的 alias 指令设置别名。

注意:在 Elixir 中,您不必在能够定义 Foo.Bar 模块之前先定义 Foo 模块,因为它们实际上是独立的。上面的代码也可以写成

defmodule Foo.Bar do
end

defmodule Foo do
  alias Foo.Bar
  # Can still access it as `Bar`
end

对嵌套模块进行别名化不会将父模块引入范围。考虑以下示例

defmodule Foo do
  defmodule Bar do
    defmodule Baz do
    end
  end
end

alias Foo.Bar.Baz
# The module `Foo.Bar.Baz` is now available as `Baz`
# However, the module `Foo.Bar` is *not* available as `Bar`

正如我们将在后面的章节中看到的,别名在宏中也起着至关重要的作用,以确保它们是卫生的。

多别名/import/require/use

可以同时 aliasimportrequireuse 多个模块。这在我们开始嵌套模块时特别有用,这在构建 Elixir 应用程序时非常常见。例如,假设您有一个应用程序,其中所有模块都嵌套在 MyApp 下,您可以像这样同时对 MyApp.FooMyApp.BarMyApp.Baz 模块进行别名化

alias MyApp.{Foo, Bar, Baz}

这样,我们就完成了对 Elixir 模块的介绍。下一个要讨论的主题是模块属性。