为了实现软件重用,Elixir提供了三种指令(alias,require和import),
外加一个宏命令use,如下:
# 给模块起别名,让它可以用 Bar 调用而非 Foo.Bar
alias Foo.Bar, as: Bar
# 确保模块已被编译且可用(通常为了宏)
require Foo
# 从 Foo 中导入函数,使之调用时不用加`Foo`前缀
import Foo
# 执行定义在 Foo 拓展点内的代码
use Foo下面我们将深入细节。记住前三个之所以称之为“指令”,
是因为它们的作用域是词法作用域(lexicla scope),
而use是一个普通拓展点(common extension point),可以将宏展开。
指令alias可以为任何模块设置别名。
想象一下之前使用过的Math模块,它针对特殊的数学运算提供了特殊的列表(list)实现:
defmodule Math do
alias Math.List, as: List
end现在,任何对List的引用将被自动变成对Math.List的引用。
如果还想访问原来的List,可以加上它的模块名前缀'Elixir':
List.flatten #=> uses Math.List.flatten
Elixir.List.flatten #=> uses List.flatten
Elixir.Math.List.flatten #=> uses Math.List.flatten注意:Elixir中定义的所有模块都被定义在Elixir命名空间内。 但为方便起见,在引用它们时,你可以省略它们的前缀‘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例子中alias指令设置的别名只在函数plus/2中有效,函数minus/2则不受影响。
Elixir提供了许多宏用于元编程(可以编写生成代码的代码)。
宏是在编译时被执行和展开的代码。
也就是说为了使用宏,你需要确保定义这个宏的模块及实现在你的代码的编译时可用(即被加载)。
这使用require指令实现:
iex> Integer.odd?(3)
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.odd?/1
iex> require Integer
nil
iex> Integer.odd?(3)
trueElixir中,Integer.odd?/1函数被定义为一个宏,因此它可以被当作卫兵表达式(guards)使用。
为了调用这个宏,首先需要使用require引用Integer模块。
总的来说,一个模块在被用到之前不需要早早地require,除非我们需要用到这个模块中定义的宏的时候。
尝试调用一个没有加载的宏时,会报出一个异常。
注意,像alias指令一样,require指令也是词法作用域的。
在后面章节我们会进一步讨论宏。
当想轻松地访问模块中的函数和宏时,可以使用import指令避免输入模块的完整名字。
例如,如果我们想多次使用List模块中的duplicate/2函数,我们可以import它:
iex> import List, only: [duplicate: 2]
List
iex> duplicate :ok, 3
[:ok, :ok, :ok]这个例子中,我们只从List模块导入了函数duplicate(元数是2的那个)。
尽管:only选项是可选的,但是仍推荐使用,以避免向当前命名空间内导入这个模块内定义的所有函数。
还有:except选项,可以排除一些函数而导入其余的。
还有选项:only,传递给它:macros或:functions,来导入该模块的所有宏或函数。
如下面例子,程序仅导入Integer模块中定义的所有的宏:
import Integer, only: :macros或者,仅导入所有的函数:
import Integer, only: :functions注意,import也遵循词法作用域,意味着我们可以在某特定函数定义内导入宏或方法:
defmodule Math do
def some_function do
import List, only: [duplicate: 2]
duplicate(:ok, 10)
end
end在这个例子中,导入的函数List.duplicate/2只在函数some_function中可见,
在该模块的其它函数中都不可用(自然,别的模块也不受影响)。
注意,若import一个模块,将自动require它。
尽管不是一条指令,use是一个宏,与帮助你在当前上下文中使用模块的require指令联系紧密。
use宏常被用来引入外部的功能到当前的词法作用域---通常是模块。
例如,在编写测试时,我们使用ExUnit框架。开发者需要使用ExUnit.Case 模块:
defmodule AssertionTest do
use ExUnit.Case, async: true
test "always pass" do
assert true
end
end在代码背后,use宏先是require所给的模块,然后在模块上调用__using__/1回调函数,
从而允许这个模块在当前上下文中注入某些代码。
比如下面这个模块:
defmodule Example do
use Feature, option: :value
end
会被编译成(即宏use扩展)
defmodule Example do
require Feature
Feature.__using__(option: :value)
end
到这里,关于Elixir的模块基本上讲得差不多了。后面会讲解模块的属性(Attribute)。
讲到这里你会问,Elixir的别名到底是什么,它是怎么实现的?
Elixir中的别名是以大写字母开头的标识符(像String, Keyword),在编译时会被转换为原子。
例如,别名‘String’默认情况下会被转换为原子:"Elixir.String":
iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String" == String
true使用alias/2指令,其实只是简单地改变了这个别名将要转换的结果。
别名会被转换为原子,是因为在Erlang虚拟机(以及上面的Elixir)中,模块是由原子表述。 例如,我们调用一个Erlang模块函数的机制是:
iex> :lists.flatten([1,[2],3])
[1, 2, 3]这也是允许我们动态调用模块函数的机制:
iex> mod = :lists
:lists
iex> mod.flatten([1,[2],3])
[1,2,3]我们只是简单地在原子:lists上调用了函数flatten。
我们已经介绍了别名,现在可以讲讲嵌套(nesting)以及它在Elixir中是如何工作的。 考虑下面的例子:
defmodule Foo do
defmodule Bar do
end
end该例子定义了两个模块:Foo和Foo.Bar。
后者在Foo中可以用Bar为名来访问,因为它们在同一个词法作用域中。
上面的代码等同于:
defmodule Elixir.Foo do
defmodule Elixir.Foo.Bar do
end
alias Elixir.Foo.Bar, as: Bar
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.Bar和MyApp.Baz提供别名:
alias MyApp.{Foo, Bar, Baz}