Elixir入门教程-模块属性

  1. 作为注释
  2. 作为常量
  3. 作为临时存储

在Elixir里,模块属性服务于三个目标:

  • 它们常常用一些信息来注释模块被用户或Erlang虚拟机使用。
  • 它们用作常量。
  • 它们被用于编译期间临时模块存储。

让我们一个一个地来仔细看看这三个方面。

作为注释

Elixr从Erlang那里带来了模块属性这个概念。例如:

1
2
3
defmodule MyServer do
@vsn 2
end

在上面的例子里,我们显式地设置模块的版本属性。@vsn 被Erlang虚拟机的代码重装载机制用来检查模块是否已经更新。如果没有指定版本,版本被设置为模块函数的MD5码校验和。

Elixir有一些保留的属性。下面介绍几个最常用的:

  • @moduledoc - 为当前模块提供文档。
  • @doc - 为紧跟该属性的函数或宏提供文档。
  • @behaviour - (注意英式拼写)用于指定一个OTP或用户自定义的行为。
  • @before_compile - 提供一个模块被编译前将被调用的钩子。这使得在编译前确切地注入函数到模块里成为可能。

@moduledoc 和 @doc 是到目前为止用得最多的属性,我们希望你大量使用它们。Elixir把文档当做一等公民并且提供许多函数访问文档。你可以参阅我们官方文档里的关于在Elixir里写文档这篇文章来获得更多信息。

我们回到前面章节里定义的Math模块,增加一些文档然后保存到math.ex文件里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defmodule Math do
@moduledoc """
Provides math-related functions.
## Examples
iex> Math.sum(1, 2)
3
"""
@doc """
Calculates the sum of two numbers.
"""
def sum(a, b), do: a + b
end

Elixir推荐用Markdown加上heredoc来写易于阅读的文档。Heredocs是多行字符串,它们起始都是三个双引号,并且保持内部文本的格式。我们可以从IEx里直接访问任何已经编译的文档:

1
2
$ elixirc math.ex
$ iex
1
2
3
4
iex> h Math # Access the docs for the module Math
...
iex> h Math.sum # Access the docs for the sum function
...

我们也提供了一个叫ExDoc的工具来将文档生成HTML页面。

你可以查阅Module模块的文档来得到一个所支持属性的完整列表。Elixir也用属性来定义 typespec

本部分覆盖了内置的属性。但是,属性也能被开发者使用或者被库扩展来支持自定义行为。

作为常量

Elixir开发者将经常使用模块属性作为常量。

1
2
3
4
defmodule MyServer do
@initial_state %{host: "147.0.0.1", port: 3456}
IO.inspect @initial_state
end

注意:和Erlang不同,用户定义的属性默认不存储在模块里。属性的值只是在编译的时候存在。开发者可以通过调用 Module.register_attribute/3 设置一个属性来使得行为更接近Erlang。

试图访问没有定义的属性将打印一个警告:

1
2
3
4
defmodule MyServer do
@unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access

最后,属性也可以在函数里被读取:

1
2
3
4
5
6
7
8
9
defmodule MyServer do
@my_data 14
def first_data, do: @my_data
@my_data 13
def second_data, do: @my_data
end
MyServer.first_data #=> 14
MyServer.second_data #=> 13

每次在函数里读取一个属性,获取到的是这个属性值的当前快照。换句话说,该值在编译时读取,而不是在运行时读取。正如我们将要看到的,这也使得在模块编译时用作存储的属性非常有用。

作为临时存储

Elixir组织里有一个项目:Plug,它就是要建立Elixir中web库和框架的共同基础。

Plug库也允许开发者定义他们自己的能在web服务器里运行的插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule MyPlug do
use Plug.Builder
plug :set_header
plug :send_ok
def set_header(conn, _opts) do
put_resp_header(conn, "x-header", "set")
end
def send_ok(conn, _opts) do
send(conn, 200, "ok")
end
end
IO.puts "Running MyPlug with Cowboy on http://localhost:4000"
Plug.Adapters.Cowboy.http MyPlug, []

在上面的例子里,我们已经用 plug/1 宏来链接当有web请求的时候将被调用的那些函数。在内部,每次你调用 plug/1,Plug库存储给定的参数到一个 @plugs 属性里。仅在模块被编译前,Plug运行一个回调来定义一个函数(call/2)处理HTTP请求。这个函数将按顺序运行@plugs属性里的所有插件。

为了理解底层的代码,我们需要宏,所以我们将在元编程指导里对这个模式进行回顾。但是此刻我们关注的是如何用模块属性作为存储来允许开发者创建DLS(领域特定语言)。

另一个例子来自ExUnit框架,它用模块属性来作为注释和存储:

1
2
3
4
5
6
7
8
defmodule MyTest do
use ExUnit.Case
@tag :external
test "contacts external service" do
# ...
end
end

在ExUnit里标签被用来注释测试用例。标签后续可以用来过滤测试用例。例如,你可以避免在你的本机上运行外部测试用例,因为它们缓慢并且依赖其他服务,尽管在你的构建系统里它们仍然是可用的。

我希望这部分内容让你大概了解Elixir如何支持元编程,以及当进行元编程的时候模块属性如何扮演一个重要角色。

在后面的章节,我们将探索结构和协议,然后再探索异常处理和其他结构,比如印记和列表解析。

原文链接: http://elixir-lang.org/getting-started/module-attributes.html