Elixir入门教程-Typespecs 和 behaviours

  1. 类型和规格说明
    1.1 函数规格说明
    1.2 定义自定义类型
    1.3 静态代码分析
  2. 行为
    2.1 定义行为
    2.2 采用行为

类型和规格说明

Elixir是一门动态类型语言,所以Elixir里的所有类型是由运行时推断出来的。尽管如此,Elixir还是有一个标记叫做 typespecs ,它被用来:

  1. 声明类型函数签名(规格说明)
  2. 声明自定义数据类型

函数规格说明

Elixir默认提供一些基本类型,比如:整形 或 pid,以及更复杂的类型:例如,round/1 函数,它返回一个浮点数最接近的整数,它的入参是一个数字(整数或浮点数)而返回的是一个整数。在它的文档里你可以看到,round/1 的类型签名写为:

1
round(number) :: integer

:: 的意思是,其左边的函数返回一个值,它的类型就是其左边的类型。

函数规格说明用指令 @spec 来写,刚好置于函数定义的前面。round/1 函数如下所示:

1
2
@spec round(number) :: integer
def round(number), do: # implementation...

Elixir也支持组合类型。例如,整数列表的类型为:[integer] 。在 typespec 的文档里,你可以看到Elixir提供的所有内建类型。

定义自定义类型

Elixir提供了很多有用的内建类型,这便于在适当的时候定义自定义类型。这可以在定义模块的时候通过 @type 指令来做到。

假设我们有一个 LousyCalculator 模块,它执行一些通常的算术运算(和、乘积,等等),但是,它不返回计算结果,而是返回一个元组,这个元组用运算结果作为第一个元素,用一个随机的评论作为第二个元素。

1
2
3
4
5
6
7
defmodule LousyCalculator do
@spec add(number, number) :: {number, String.t}
def add(x, y), do: {x + y, "You need a calculator to do that?!"}
@spec multiply(number, number) :: {number, String.t}
def multiply(x, y), do: {x * y, "Jeez, come on!"}
end

如你在例子看到的,元组是一个组合类型,并且每一个元组在其内部通过类型来定义。要理解为什么String.t不写作String,请看在typespec文档里的说明

定义函数规格说明以上述例子的方式是可行的,不过很快就变得令人烦恼了,因为我们正一遍又一遍地重复输入 {number, String.t} 。我们可以用 @type 指令来声明我们自己的自定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
defmodule LousyCalculator do
@typedoc """
Just a number followed by a string.
"""
@type number_with_remark :: {number, String.t}
@spec add(number, number) :: number_with_remark
def add(x, y), do: {x + y, "You need a calculator to do that?"}
@spec multiply(number, number) :: number_with_remark
def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end

@typedoc 指令相似于 @doc 和 @moduledoc 指令,它被用来注释自定义类型。

通过 @type 定义的自定义类型被导出而且在它们被定义的模块外部也可用:

1
2
3
4
5
6
7
defmodule QuietCalculator do
@spec add(number, number) :: number
def add(x, y), do: make_quiet(LousyCalculator.add(x, y))
@spec make_quiet(LousyCalculator.number_with_remark) :: number
defp make_quiet({num, _remark}), do: num
end

如果你想保留自定义类型为私有的,你可以用 @typep 指令替代 @type 指令。

静态代码分析

类型规格说明不仅是对开发人员有用的附加文档。例如,Erlang工具 Dialyzer 使用类型规格说明来进行代码的静态分析。这就是为什么在 QuietCalculator 例子里,我们为 make_quiet/1 函数写规格说明,即使它被定义为私有函数。

行为

许多模块共享相同的公共API。我们看看Plug,正如它自己的描述所声明,它是web应用里可组合的模块的规格说明。每一个plug是一个模块,它必须实现最少两个公共函数:init/1 和 call/2 。

行为提供一种方式:

  • 定义必须被一个模块实现的一个函数集合;
  • 确定一个模块实现这个集合里的所有函数。

如果你愿意,你可以认为行为就像Java这种面向对象语言里的接口:一个模块必须实现的函数签名的集合。

定义行为

假如我们想实现一些分析器,它们每一个都分析结构化的数据:例如,一个JSON分析器和一个YAML分析器。这两个解析器将有相同的行为方式:两者都提供一个parse/1 函数和一个 extensions/0 函数。parse/1 函数将返回一个Elixir的结构化数据描述,而 extensions/0 函数将返回可被用于每种数据类型(例如,.json对应于JSON文件)的文件扩展名的列表。

我们可以创建一个 Parser 行为:

1
2
3
4
defmodule Parser do
@callback parse(String.t) :: any
@callback extensions() :: [String.t]
end

采用 Parser 行为的模块将必须实现所有用 @callback 指令定义的函数。如你所见,@callback 指令期望有一个函数名以及如我们前面所看到的 @spec 指令用到的函数规格说明。

采用行为

采用一个行为很简单:

1
2
3
4
5
6
defmodule JSONParser do
@behaviour Parser
def parse(str), do: # ... parse JSON
def extensions, do: ["json"]
end
1
2
3
4
5
6
defmodule YAMLParser do
@behaviour Parser
def parse(str), do: # ... parse YAML
def extensions, do: ["yml"]
end

如果一个模块采用一个给定的行为却没有实现行为需要的所有回调函数中的一个,则一个编译期警告将被生成。

原文链接: http://elixir-lang.org/getting-started/typespecs-and-behaviours.html