类型规范和行为 | Typespecs and behaviours
类型和规格
Elixir 是一种动态类型语言,因此 Elixir 中的所有类型均由运行时推断。尽管如此,Elixir 附带了 typespecs,这是一种符号,用于:
- 声明类型化函数签名(规范);
2. 声明自定义数据类型。
功能规格
默认情况下,Elixir 提供了一些基本类型,比如integer
或者pid
更复杂的类型:例如,将 float 四舍五入到最接近的整数number
的round/1
函数将一个参数(一个 integer
或一个 float
)作为参数并返回一个integer
。正如你可以看到它的文档中,round/1
的类型签名写成:
round(number) :: integer
::
意味着左侧的函数返回右侧类型的值。函数规范是用@spec
指令编写的,放在函数定义之前。该round/1
函数可以写为:
@spec round(number) :: integer
def round(number), do: # implementation...
Elixir也支持复合类型。例如,整数列表有类型[integer]
。您可以在类型规格文档中看到 Elixir 提供的所有内置类型。
定义自定义类型
虽然 Elixir 提供了许多有用的内置类型,但适当时可以方便地定义自定义类型。这可以通过@type
指令定义模块完成。
假设我们有一个LousyCalculator
模块,它执行通常的算术运算(sum,product 等),但是不是返回数字,而是返回带有操作结果的元组作为第一个元素,并将随机备注作为第二个元素返回。
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
,请再次查看typespecs 文档中的注释。
通过这种方式定义函数规范,但它很快就变得令人讨厌,因为我们{number,String.t}
一遍又一遍地重复类型。我们可以使用该@type
指令来声明我们自己的自定义类型。
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
被导出并在其定义的模块外可用:
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
。
静态代码分析
Typespecs 不仅对开发人员有用,还可以作为附加文档。Erlang 的工具透析器,例如,使用 typespecs 为了执行的代码的静态分析。这就是为什么,在这个QuietCalculator
例子中,我们为make_quiet/1
函数写了一个规范,即使它被定义为一个私有函数。
行为
许多模块共享相同的公共 API。看看Plug,就像它的说明所述,它是一个 Web 应用程序中可组合模块的规范。每个插头都是一个必须至少实现两个公共功能的模块:init/1
和call/2
。
行为提供了一种方法:
- 定义一组必须由模块实现的功能;
- 确保模块实现该组中的所有功能。
如果必须的话,您可以考虑像 Java 这样的面向对象语言中的接口这样的行为:模块必须实现的一组函数签名。
定义行为
假设我们要实现一堆解析器,每个解析器都解析结构化数据:例如,一个 JSON 解析器和一个 MessagePack 解析器。每两个解析器会表现得同样的方式:双方将提供parse/1
功能和extensions/0
功能。该parse/1
函数将返回结构化数据的Elixir表示,而extensions/0
函数将返回可用于每种类型数据(例如,.json
用于 JSON 文件)的文件扩展名列表。
我们可以创建一个Parser
行为:
defmodule Parser do
@callback parse(String.t) :: {:ok, term} | {:error, String.t}
@callback extensions() :: [String.t]
end
采用该Parser
行为的模块将不得不实现用该@callback
指令定义的所有功能。正如你所看到的,@callback
期望一个函数名称,但也是一个函数规范,就像@spec
我们上面看到的指令一样。另请注意,该term
类型用于表示解析的值。在 Elixir 中,该term
类型是代表任何类型的快捷方式。
采取行为
采取行为很简单:
defmodule JSONParser do
@behaviour Parser
def parse(str), do: # ... parse JSON
def extensions, do: ["json"]
end
defmodule YAMLParser do
@behaviour Parser
def parse(str), do: # ... parse YAML
def extensions, do: ["yml"]
end
如果采用给定行为的模块没有实现该行为所需的回调之一,则会生成编译时警告。
动态调度
行为经常与动态调度一起使用。例如,我们可以为parse!
调用给Parser
定实现的模块添加一个函数,并:ok
在以下情况下返回结果或引发:error
:
defmodule Parser do
@callback parse(String.t) :: {:ok, term} | {:error, String.t}
@callback extensions() :: [String.t]
def parse!(implementation, contents) do
case implementation.parse(contents) do
{:ok, data} -> data
{:error, error} -> raise ArgumentError, "parsing error: #{error}"
end
end
end
请注意,您不需要定义行为以便在模块上进行动态分派,但这些功能常常并行不悖。
本文档系腾讯云开发者社区成员共同维护,如有问题请联系 cloudcommunity@tencent.com