13-别名和代码引用

别名
require
import
别名机制
嵌套

为了实现软件重用,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

该例子定义了两个模块FooFoo.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.BarMyApp.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)。

讨论到这里,模块基本上讲得差不多了。之后会讲解模块的属性。