20-Typespecs和behaviors

类型(type)和规格说明(spec)

Elixir是一门动态类型语言,Elixir中所有数据类型都是在运行时动态推定的。 然而,Elixir还提供了 typespecs 标记,用来:

  1. 声明自定义数据类型
  2. 声明含有显式类型说明的函数签名(即函数的规格说明)

函数的规格说明(spec)

默认地,Elixir提供了一些基础数据类型,表示为 integer 或者 pid。 还有一些复杂情形:如函数round/1为例,它对一个float类型的数值四舍五入。 它以一个number(一个integerfloat)作为参数,返回一个integer

那么,它在round函数的文档 里面记载的函数签名为:

  1. round(number) :: integer

:: 表示其左边的函数 返回 一个其右面声明的类型的值。函数名后面括号中是参数类型的列表。

如果想特别注明某个函数的参数类型及返回值类型,那么可以在定义函数的时候, 在函数前面使用@spec指令附加上函数的规格说明(spec)。

比如,在函数库源码中,函数round/1是这么写的:

  1. @spec round(number) :: integer
  2. def round(number), do: # 具体实现 ...

Elixir还支持组合类型。例如,整数的列表,它的类型表示为[integer]。 可以阅读typespecs的文档 查看Elixir提供的所有内建类型的表示方法。

定义自定义类型

Elixir提供了许多有用的内建类型,而且也方便创建自定义类型应用于特定场景。 方法是在定义的时候,加上@type指令。

比如我们有个模块叫做LuosyCalculator,可以执行常见的算术计算(如求和、计算乘积等)。 但是,它的函数不是返回结果数值,而是返回一个元組, 该元組第一个元素是计算结果,第二个是随机的文字记号。

  1. defmodule LousyCalculator do
  2. @spec add(number, number) :: {number, String.t}
  3. def add(x, y), do: { x + y, "你用计算器算这个?!" }
  4. @spec multiply(number, number) :: {number, String.t}
  5. def multiply(x, y), do: { x * y, "老天,不是吧?!" }
  6. end

从例子中可以看出,元组是复合类型。每个元组都定义了其具体元素类型。 至于为何是String.t而不是string的原因,可以参考 这篇文章, 此处不多说明。

像这样定义函数规格说明是没问题,但是一次次重复写这种复合类型的 表示方法{number, String.t},很快会厌烦的吧。 我们可以使用@type指令来声明我们自定义的类型:

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

指令@typedoc,和@doc@moduledoc指令类似,用来解释说明自定义的类型,放在@type前面。

另外,通过@type定义的自定义类型,实际上也是模块的成员,可以被外界访问:

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

如果想要将某个自定义类型保持私有,可以使用 @typep 指令代替 @type

静态代码分析

给函数等元素标记类型或者签名的作用,不仅仅是被用来作为程序文档说明。举个例子, Erlang工具[Dialyzer]Dialyzer 通过这些类型或者签名标记,进行代码静态分析。 这就是为什么我们在 QuiteCalculator 例子中, 即使 make_quite/1 是个私有函数,也写了函数规格说明。

行为(behavior)

许多模块公用相同的公共API。可以参考下Plug, 正如它的描述所言,是一个用于互联网应用的、可编辑的模块的规格声明。 每个所谓plug就是一个必须实现至少两个公共函数:init/1call/2的模块。

行为提供了一种方法,用来:

  • 定义一系列必须实现的函数
  • 确保模块实现所有这些函数

你也可以把这些行为想象为面向对象语言里的接口:模块必须实现的一系列函数签名。

定义行为(Defining behaviors)

假如说我们希望实现一系列parser,每个parser解析结构化的数据:比如,一个JSON parser或是YAML parser。 这两个parser的行为几近相同: 它们都提供一个parse/1函数和一个extensions/0函数。parse/1函数返回一个数据对应的Elixir表达。 而extensions/0函数返回一个可被其解析的文件的扩展名列表(如,JSON文件是.json)。

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

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

那么,采用Parser这个行为的模块,必须实现所有被@callback指令标记的函数。正如你所看到的, @callback指令不但可以接受一个函数名,还可以接受一个函数规格定义(我们在本文开头讲述的,函数的spec)。

采用行为(adopting behavior)

模块采用一个行为的语法非常直白:

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

如果一个模块采用了一个尚未完全实现其所需回调方法的行为(behavior),这将生成一个编译时错误。