13-别名和代码引用
为了实现软件重用,Elixir提供了三种指令(directives)。 之所以称之为“指令”,是因为它们的作用域是词法作用域(lexicla scope)。
13.1-别名
宏alias
可以为任何模块名设置别名。
想象一下Math模块,它针对特殊的数学运算使用了特殊的列表实现:
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
注意,别名是词法作用域。即,你在某个函数中设置别名:
defmodule Math do
def plus(a, b) do
alias Math.List
# ...
end
def minus(a, b) do
# ...
end
end
例子中alias
指令只在函数plus/2
中有用,minus/2
不受影响。
13.2-require
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)
true
Elixir中,Integer.odd?/1
函数被定义为一个宏,它可以被当作卫兵表达式(guards)使用。
为了调用这个宏,首先得用require
引用了Integer模块。
总的来说,宏在被用到之前不需要早早被require引用进来,除非我们想让这个宏在整个模块中可用。
尝试调用一个没有引入的宏会导致报错。
注意,像alias
指令一样,require
也是词法作用域的。
下文中我们会进一步讨论宏。
13.3-import
当想轻松地访问别的模块中的函数和宏时,可以使用import
指令加上宏定义模块的完整名字。
例如,如果我们想多次使用List模块中的duplicate
函数,我们可以这么import它:
iex> import List, only: [duplicate: 2]
nil
iex> duplicate :ok, 3
[:ok, :ok, :ok]
这个例子中,我们只从List模块导入了函数duplicate/2
。
尽管only:
选项是可选的,但是推荐使用。
除了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]
# call duplicate
end
end
在此例子中,导入的函数List.duplicate/2
只在那个函数中可见。
该模块的其它函数中都不可用(自然,别的模块也不受影响)。
注意,若import一个模块,将自动require它。
13.4-别名机制
讲到这里你会问,Elixir的别名到底是什么,它是怎么实现的?
Elixir中的别名是以大写字母开头的标识符(像String, Keyword等等),在编译时会被转换为原子。
例如,别名‘String’会被转换为:"Elixir.String"
:
iex> is_atom(String)
true
iex> to_string(String)
"Elixir.String"
iex> :"Elixir.String"
String
使用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
。
13.5-嵌套
介绍了别名,现在可以讲讲嵌套(nesting)以及它在Elixir中是如何工作的。
考虑下面的例子:
defmodule Foo do
defmodule Bar do
end
end
该例子定义了两个模块Foo和Foo.Bar。 后者在Foo中可以用Bar为名来访问,因为它们在同一个词法作用域中。 如果之后开发者决定把Bar模块挪到另一个文件中,那它就需要以全名(Foo.Bar)或者别名来指代。
换句话说,上面的代码等同于:
defmodule Elixir.Foo do
defmodule Elixir.Foo.Bar do
end
alias Elixir.Foo.Bar, as: Bar
end
13.6-多个
到Elixir v1.2,可以一次使用alias,import,require多个模块。这在建立Elixir程序的时候很常见,特别是使用嵌套的时候是最有用了。例如,假设你的程序所有模块都嵌套在MyApp
下,需要使用别名MyApp.Foo
,MyApp.Bar
和MyApp.Baz
,可以像下面一次完成:
alias MyApp.{Foo, Bar, Baz}
13.7-use
use用于请求在当前上下文中允许使用其他模块,是一个宏不是指令。开发者频繁使用use
宏在当前上下文范围内引入其他功能,通常是模块。
例如,在编写测试时需要使用ExUni框架,开发者需要使用ExUnit.Case
模块:
defmodule AssertionTest do
use ExUnit.Case, async: true
test "always pass" do
assert true
end
end
在后面,use
请求需要的模块,然后调用__using__/1
回调函数,允许模块在当前上下文中注入这些代码。总体说,如下模块:
defmodule Example do
use Feature, option: :value
end
会编译成
defmodule Example do
require Feature
Feature.__using__(option: :value)
end
在以后章节我们可以看到,别名在宏机制中扮演了很重要的角色,来保证宏是干净的(hygienic)。
讨论到这里,模块基本上讲得差不多了。之后会讲解模块的属性。