13-alias,require和import

为了实现软件重用,Elixir提供了三种指令(aliasrequireimport), 外加一个宏命令use,如下:

  1. # 给模块起别名,让它可以用 Bar 调用而非 Foo.Bar
  2. alias Foo.Bar, as: Bar
  3. # 确保模块已被编译且可用(通常为了宏)
  4. require Foo
  5. # 从 Foo 中导入函数,使之调用时不用加`Foo`前缀
  6. import Foo
  7. # 执行定义在 Foo 拓展点内的代码
  8. use Foo

下面我们将深入细节。记住前三个之所以称之为“指令”, 是因为它们的作用域是词法作用域(lexicla scope), 而use是一个普通拓展点(common extension point),可以将宏展开。

alias

指令alias可以为任何模块设置别名。 想象一下之前使用过的Math模块,它针对特殊的数学运算提供了特殊的列表(list)实现:

  1. defmodule Math do
  2. alias Math.List, as: List
  3. end

现在,任何对List的引用将被自动变成对Math.List的引用。 如果还想访问原来的List,可以加上它的模块名前缀’Elixir’:

  1. List.flatten #=> uses Math.List.flatten
  2. Elixir.List.flatten #=> uses List.flatten
  3. Elixir.Math.List.flatten #=> uses Math.List.flatten

注意:Elixir中定义的所有模块都被定义在Elixir命名空间内。 但为方便起见,在引用它们时,你可以省略它们的前缀‘Elixir’。

别名常被使用于定义快捷方式。实际应用中,不带:as选项调用alias会 自动将别名设置为该模块名称的最后一部分:

  1. alias Math.List

就相当于:

  1. alias Math.List, as: List

注意,alias词法作用域。也就是说,当你在某个函数中设置别名:

  1. defmodule Math do
  2. def plus(a, b) do
  3. alias Math.List
  4. # ...
  5. end
  6. def minus(a, b) do
  7. # ...
  8. end
  9. end

例子中alias指令设置的别名只在函数plus/2中有效,函数minus/2则不受影响。

require

Elixir提供了许多宏用于元编程(可以编写生成代码的代码)。

宏是在编译时被执行和展开的代码。 也就是说为了使用宏,你需要确保定义这个宏的模块及实现在你的代码的编译时可用(即被加载)。 这使用require指令实现:

  1. iex> Integer.odd?(3)
  2. ** (CompileError) iex:1: you must require Integer before invoking the macro Integer.odd?/1
  3. iex> require Integer
  4. nil
  5. iex> Integer.odd?(3)
  6. true

Elixir中,Integer.odd?/1函数被定义为一个宏,因此它可以被当作卫兵表达式(guards)使用。 为了调用这个宏,首先需要使用require引用Integer模块。

总的来说,一个模块在被用到之前不需要早早地require,除非我们需要用到这个模块中定义的宏的时候。 尝试调用一个没有加载的宏时,会报出一个异常。 注意,像alias指令一样,require指令也是词法作用域的。 在后面章节我们会进一步讨论宏。

import

当想轻松地访问模块中的函数和宏时,可以使用import指令避免输入模块的完整名字。 例如,如果我们想多次使用List模块中的duplicate/2函数,我们可以import它:

  1. iex> import List, only: [duplicate: 2]
  2. List
  3. iex> duplicate :ok, 3
  4. [:ok, :ok, :ok]

这个例子中,我们只从List模块导入了函数duplicate(元数是2的那个)。 尽管:only选项是可选的,但是仍推荐使用,以避免向当前命名空间内导入这个模块内定义的所有函数。 还有:except选项,可以排除一些函数而导入其余的。

还有选项:only,传递给它:macros:functions,来导入该模块的所有宏或函数。 如下面例子,程序仅导入Integer模块中定义的所有的宏:

  1. import Integer, only: :macros

或者,仅导入所有的函数:

  1. import Integer, only: :functions

注意,import也遵循词法作用域,意味着我们可以在某特定函数定义内导入宏或方法:

  1. defmodule Math do
  2. def some_function do
  3. import List, only: [duplicate: 2]
  4. duplicate(:ok, 10)
  5. end
  6. end

在这个例子中,导入的函数List.duplicate/2只在函数some_function中可见, 在该模块的其它函数中都不可用(自然,别的模块也不受影响)。

注意,若import一个模块,将自动require它。

use

尽管不是一条指令,use是一个宏,与帮助你在当前上下文中使用模块的require指令联系紧密。 use宏常被用来引入外部的功能到当前的词法作用域—-通常是模块。

例如,在编写测试时,我们使用ExUnit框架。开发者需要使用ExUnit.Case 模块:

  1. defmodule AssertionTest do
  2. use ExUnit.Case, async: true
  3. test "always pass" do
  4. assert true
  5. end
  6. end

在代码背后,use宏先是require所给的模块,然后在模块上调用__using__/1回调函数, 从而允许这个模块在当前上下文中注入某些代码。

比如下面这个模块:

  1. defmodule Example do
  2. use Feature, option: :value
  3. end

会被编译成(即宏use扩展)

  1. defmodule Example do
  2. require Feature
  3. Feature.__using__(option: :value)
  4. end

到这里,关于Elixir的模块基本上讲得差不多了。后面会讲解模块的属性(Attribute)。

别名机制

讲到这里你会问,Elixir的别名到底是什么,它是怎么实现的?

Elixir中的别名是以大写字母开头的标识符(像String, Keyword),在编译时会被转换为原子。 例如,别名‘String’默认情况下会被转换为原子:"Elixir.String"

  1. iex> is_atom(String)
  2. true
  3. iex> to_string(String)
  4. "Elixir.String"
  5. iex> :"Elixir.String" == String
  6. true

使用alias/2指令,其实只是简单地改变了这个别名将要转换的结果。

别名会被转换为原子,是因为在Erlang虚拟机(以及上面的Elixir)中,模块是由原子表述。 例如,我们调用一个Erlang模块函数的机制是:

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

这也是允许我们动态调用模块函数的机制:

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

我们只是简单地在原子:lists上调用了函数flatten

模块嵌套

我们已经介绍了别名,现在可以讲讲嵌套(nesting)以及它在Elixir中是如何工作的。 考虑下面的例子:

  1. defmodule Foo do
  2. defmodule Bar do
  3. end
  4. end

该例子定义了两个模块:FooFoo.Bar。 后者在Foo中可以用Bar为名来访问,因为它们在同一个词法作用域中。 上面的代码等同于:

  1. defmodule Elixir.Foo do
  2. defmodule Elixir.Foo.Bar do
  3. end
  4. alias Elixir.Foo.Bar, as: Bar
  5. end

如果之后开发者决定把Bar模块定义挪出Foo模块的定义,但是在Foo中仍然使用Bar来引用, 那它就需要以全名(Foo.Bar)来命名,或者向上文提到的,在Foo中设置个别名来指代。

注意: 在Elixir中,你并不是必须在定义Foo.Bar模块之前定义Foo模块, 因为编程语言会将所有模块名翻译成原子。 你可以定义任意嵌套的模块而不需要注意其名称链上的先后顺序 (比如,在定义Foo.Bar.Baz前不需要提前定义foo或者Foo.Bar)。

在后面几章我们会看到,别名在宏里面也扮演着重要角色,来保证它们是“干净”(hygienic)的。

一次、多个

从Elixir v1.2版本开始,可以一次性使用alias,import,require操作多个模块。 这在定义和使用嵌套模块的时候非常有用,这也是在构建Elixir程序的常见情形。

例如,假设你的程序所有模块都嵌套在MyApp下, 你可以一次同时给三个模块:MyApp.Foo,MyApp.BarMyApp.Baz提供别名:

  1. alias MyApp.{Foo, Bar, Baz}