查看源代码 类型规格参考
Elixir 提供了一种用于声明类型和规格的符号。本文档介绍了它们的用法和语法。
Elixir 是一种动态类型语言,因此编译器永远不会使用类型规格来优化或修改代码。但是,使用类型规格仍然有用,因为
类型规格(通常称为类型规格)在不同的上下文中使用以下属性定义
@type
@opaque
@typep
@spec
@callback
@macrocallback
此外,您可以使用 @typedoc
来记录自定义的 @type
定义。
有关定义类型和类型规格的更多信息,请参阅下面的“用户定义类型”和“定义规格”子部分。
一个简单的例子
defmodule StringHelpers do
@typedoc "A word from the dictionary"
@type word() :: String.t()
@spec long_word?(word()) :: boolean()
def long_word?(word) when is_binary(word) do
String.length(word) > 8
end
end
在上面的示例中
我们声明了一个新的类型(
word()
),它等效于字符串类型(String.t()
)。我们使用
@typedoc
描述了该类型,它将被包含在生成的文档中。我们指定了
long_word?/1
函数接受一个word()
类型的参数,并返回一个布尔值(boolean()
),即true
或false
。
类型及其语法
Elixir 提供的类型规格语法类似于 Erlang 中的语法。Erlang 中提供的大多数内置类型(例如,pid()
)都以相同的方式表示:pid()
(或简写为 pid
)。参数化类型(如 list(integer)
)也受支持,远程类型(如 Enum.t()
)也是如此。整数和原子字面量允许作为类型(例如,1
、:atom
或 false
)。所有其他类型都是从预定义类型的并集构建的。一些简写形式是允许的,例如 [...]
、<<>>
和 {...}
。
表示类型并集的符号是管道符 |
。例如,类型规格 type :: atom() | pid() | tuple()
创建了一个可以是 atom
、pid
或 tuple
的类型 type
。这通常在其他语言中称为 总类型
基本类型
type ::
any() # the top type, the set of all terms
| none() # the bottom type, contains no terms
| atom()
| map() # any map
| pid() # process identifier
| port() # port identifier
| reference()
| tuple() # tuple of any size
## Numbers
| float()
| integer()
| neg_integer() # ..., -3, -2, -1
| non_neg_integer() # 0, 1, 2, 3, ...
| pos_integer() # 1, 2, 3, ...
## Lists
| list(type) # proper list ([]-terminated)
| nonempty_list(type) # non-empty proper list
| maybe_improper_list(content_type, termination_type) # proper or improper list
| nonempty_improper_list(content_type, termination_type) # improper list
| nonempty_maybe_improper_list(content_type, termination_type) # non-empty proper or improper list
| Literals # Described in section "Literals"
| BuiltIn # Described in section "Built-in types"
| Remotes # Described in section "Remote types"
| UserDefined # Described in section "User-defined types"
字面量
以下字面量也支持在类型规格中
type :: ## Atoms
:atom # atoms: :foo, :bar, ...
| true | false | nil # special atom literals
## Bitstrings
| <<>> # empty bitstring
| <<_::size>> # size is 0 or a positive integer
| <<_::_*unit>> # unit is an integer from 1 to 256
| <<_::size, _::_*unit>>
## (Anonymous) Functions
| (-> type) # zero-arity, returns type
| (type1, type2 -> type) # two-arity, returns type
| (... -> type) # any arity, returns type
## Integers
| 1 # integer
| 1..10 # integer from 1 to 10
## Lists
| [type] # list with any number of type elements
| [] # empty list
| [...] # shorthand for nonempty_list(any())
| [type, ...] # shorthand for nonempty_list(type)
| [key: value_type] # keyword list with optional key :key of value_type
## Maps
| %{} # empty map
| %{key: value_type} # map with required key :key of value_type
| %{key_type => value_type} # map with required pairs of key_type and value_type
| %{required(key_type) => value_type} # map with required pairs of key_type and value_type
| %{optional(key_type) => value_type} # map with optional pairs of key_type and value_type
| %SomeStruct{} # struct with all fields of any type
| %SomeStruct{key: value_type} # struct with required key :key of value_type
## Tuples
| {} # empty tuple
| {:ok, type} # two-element tuple with an atom and any type
内置类型
以下类型也由 Elixir 提供,作为对上述基本类型和字面量类型的简写。
内置类型 | 定义为 |
---|---|
term() | any() |
arity() | 0..255 |
as_boolean(t) | t |
binary() | <<_::_*8>> |
nonempty_binary() | <<_::8, _::_*8>> |
bitstring() | <<_::_*1>> |
nonempty_bitstring() | <<_::1, _::_*1>> |
boolean() | true | false |
byte() | 0..255 |
char() | 0..0x10FFFF |
charlist() | [char()] |
nonempty_charlist() | [char(), ...] |
fun() | (... -> any) |
function() | fun() |
identifier() | pid() | port() | reference() |
iodata() | iolist() | binary() |
iolist() | maybe_improper_list(byte() | binary() | iolist(), binary() | []) |
keyword() | [{atom(), any()}] |
keyword(t) | [{atom(), t}] |
list() | [any()] |
nonempty_list() | nonempty_list(any()) |
maybe_improper_list() | maybe_improper_list(any(), any()) |
nonempty_maybe_improper_list() | nonempty_maybe_improper_list(any(), any()) |
mfa() | {module(), atom(), arity()} |
module() | atom() |
no_return() | none() |
node() | atom() |
number() | integer() | float() |
struct() | %{:__struct__ => atom(), optional(atom()) => any()} |
timeout() | :infinity | non_neg_integer() |
as_boolean(t)
用于向用户发出信号,表明给定值将被视为布尔值,其中 nil
和 false
将被评估为 false
,其他所有内容将被评估为 true
。例如,Enum.filter/2
具有以下规格:filter(t, (element -> as_boolean(term))) :: list
。
远程类型
任何模块都可以定义自己的类型,Elixir 中的模块也不例外。例如,Range
模块定义了一个 t/0
类型来表示范围:此类型可以被称为 Range.t/0
。类似地,字符串是 String.t/0
,等等。
映射
映射中的键类型允许重叠,如果重叠,则最左边的键优先。如果映射值包含不在允许的映射键中的键,则该值不属于该类型。
如果要表示允许以前在映射中未定义的键,则通常用 optional(any) => any
结束映射类型。
请注意,map()
的语法表示是 %{optional(any) => any}
,而不是 %{}
。符号 %{}
指定空映射的单例类型。
关键字列表
除了 keyword()
和 keyword(t)
之外,为预期关键字列表组合规格可能会有所帮助。例如
@type option :: {:name, String.t} | {:max, pos_integer} | {:min, pos_integer}
@type options :: [option()]
这清楚地表明,只允许这些选项,没有选项是必需的,顺序无关紧要。
它还允许与现有类型组合。例如
type option :: {:my_option, String.t()} | GenServer.option()
@spec start_link([option()]) :: GenServer.on_start()
def start_link(opts) do
{my_opts, gen_server_opts} = Keyword.split(opts, [:my_option])
GenServer.start_link(__MODULE__, my_opts, gen_server_opts)
end
用户定义类型
可以使用 @type
、@typep
和 @opaque
模块属性来定义新类型
@type type_name :: type
@typep type_name :: type
@opaque type_name :: type
使用 @typep
定义的类型是私有的。使用 @opaque
定义的不透明类型是一种类型,其中类型的内部结构将不可见,但该类型仍然是公开的。
通过将变量定义为参数来参数化类型;这些变量随后可用于定义类型。
@type dict(key, value) :: [{key, value}]
定义规格
函数的规格可以按如下方式定义
@spec function_name(type1, type2) :: return_type
可以使用守卫来限制作为函数参数给出的类型变量。
@spec function(arg) :: [arg] when arg: atom
如果要指定多个变量,请用逗号隔开它们。
@spec function(arg1, arg2) :: {arg1, arg2} when arg1: atom, arg2: integer
可以使用 var
定义没有限制的类型变量。
@spec function(arg) :: [arg] when arg: var
此守卫符号仅适用于 @spec
、@callback
和 @macrocallback
。
您也可以使用 arg_name :: arg_type
语法在类型规格中命名参数。这在文档中特别有用,因为它可以区分具有相同类型的多个参数(或类型定义中具有相同类型的多个元素)
@spec days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer
@type color :: {red :: integer, green :: integer, blue :: integer}
规格可以重载,就像普通函数一样。
@spec function(integer) :: atom
@spec function(atom) :: integer
行为
Elixir(和 Erlang)中的行为是一种将组件的通用部分(成为行为模块)与特定部分(成为回调模块)分隔和抽象的方式。
行为模块定义了一组函数和宏(称为回调),实现该行为的回调模块必须导出这些函数和宏。此“接口”标识了组件的特定部分。例如,GenServer
行为和函数抽象了“服务器”进程可能想要实现的所有消息传递(发送和接收)和错误报告,以及特定部分,例如此服务器进程必须执行的操作。
假设我们要实现一些解析器,每个解析器解析结构化数据:例如,一个 JSON 解析器和一个 MessagePack 解析器。这两个解析器都将表现相同:它们都将提供一个 parse/1
函数和一个 extensions/0
函数。 parse/1
函数将返回结构化数据的 Elixir 表示形式,而 extensions/0
函数将返回可用于每种数据类型的文件扩展名列表(例如,JSON 文件的 .json
)。
我们可以创建一个 Parser
行为
defmodule Parser do
@doc """
Parses a string.
"""
@callback parse(String.t) :: {:ok, term} | {:error, atom}
@doc """
Lists all supported file extensions.
"""
@callback extensions() :: [String.t]
end
如上面的示例所示,定义回调是定义该回调的规格问题,该规格由以下部分组成
- 回调名称(示例中的
parse
或extensions
) - 回调必须接受的参数(
String.t
) - 回调返回值的预期类型
采用 Parser
行为的模块必须实现所有使用 @callback
属性定义的函数。如您所见,@callback
期望一个函数名,但也期望一个函数规格,就像我们上面在 @spec
属性中使用的规格一样。
实现行为
实现行为非常简单
defmodule JSONParser do
@behaviour Parser
@impl Parser
def parse(str), do: {:ok, "some json " <> str} # ... parse JSON
@impl Parser
def extensions, do: [".json"]
end
defmodule CSVParser do
@behaviour Parser
@impl Parser
def parse(str), do: {:ok, "some csv " <> str} # ... parse CSV
@impl Parser
def extensions, do: [".csv"]
end
如果采用给定行为的模块没有实现该行为要求的某个回调,则会生成编译时警告。
此外,使用 @impl
,您还可以以显式方式确保您正在从给定行为中实现正确的回调。例如,以下解析器同时实现了 parse
和 extensions
。但是,由于输入错误,BADParser
正在实现 parse/0
而不是 parse/1
。
defmodule BADParser do
@behaviour Parser
@impl Parser
def parse, do: {:ok, "something bad"}
@impl Parser
def extensions, do: ["bad"]
end
此代码会生成一个警告,让您知道您错误地实现了 parse/0
而不是 parse/1
。您可以在 模块文档 中了解更多有关 @impl
的信息。
使用行为
行为很有用,因为你可以将模块作为参数传递,然后你可以*回调*到行为中指定的任何函数。例如,我们可以有一个接收文件名和多个解析器的函数,并根据文件扩展名解析文件。
@spec parse_path(Path.t(), [module()]) :: {:ok, term} | {:error, atom}
def parse_path(filename, parsers) do
with {:ok, ext} <- parse_extension(filename),
{:ok, parser} <- find_parser(ext, parsers),
{:ok, contents} <- File.read(filename) do
parser.parse(contents)
end
end
defp parse_extension(filename) do
if ext = Path.extname(filename) do
{:ok, ext}
else
{:error, :no_extension}
end
end
defp find_parser(ext, parsers) do
if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do
{:ok, parser}
else
{:error, :no_matching_parser}
end
end
你也可以直接调用任何解析器:CSVParser.parse(...)
。
注意,你不需要定义行为来动态分发模块,但这些功能通常是相辅相成的。
可选回调
可选回调是回调模块可以选择实现的回调,但不是必需的。通常,行为模块根据配置知道是否应该调用这些回调,或者它们使用 function_exported?/3
或 macro_exported?/3
检查回调是否已定义。
可选回调可以通过 @optional_callbacks
模块属性定义,该属性必须是一个关键字列表,其中函数或宏名称作为键,参数个数作为值。例如
defmodule MyBehaviour do
@callback vital_fun() :: any
@callback non_vital_fun() :: any
@macrocallback non_vital_macro(arg :: any) :: Macro.t
@optional_callbacks non_vital_fun: 0, non_vital_macro: 1
end
Elixir 标准库中可选回调的一个例子是 GenServer.format_status/2
.
检查行为
@callback
和 @optional_callbacks
属性用于在定义模块上创建 behaviour_info/1
函数。此函数可用于检索该模块定义的回调和可选回调。
例如,对于上面“可选回调”中定义的 MyBehaviour
模块
MyBehaviour.behaviour_info(:callbacks)
#=> [vital_fun: 0, "MACRO-non_vital_macro": 2, non_vital_fun: 0]
MyBehaviour.behaviour_info(:optional_callbacks)
#=> ["MACRO-non_vital_macro": 2, non_vital_fun: 0]
使用 iex
时,还提供 IEx.Helpers.b/1
助手。
陷阱
使用类型规范时,有一些已知的陷阱,这些陷阱将在下面介绍。
string()
类型
Elixir 不鼓励使用 string()
类型。 string()
类型指的是 Erlang 字符串,在 Elixir 中被称为“字符列表”。它们不指的是 Elixir 字符串,Elixir 字符串是 UTF-8 编码的二进制文件。为了避免混淆,如果你尝试使用 string()
类型,Elixir 会发出警告。你应该根据需要使用 charlist()
、nonempty_charlist()
、binary()
或 String.t()
,或者使用这些类型的几种文字表示形式。
注意 String.t()
和 binary()
对分析工具来说是等价的。尽管对于阅读文档的人来说,String.t()
表示它是 UTF-8 编码的二进制文件。
引发错误的函数
类型规范不需要表明函数可以引发错误;任何函数在给定无效输入时都可能随时失败。在过去,Elixir 标准库有时使用 no_return()
来表示这一点,但这些用法已被移除。
no_return()
类型也不应该用于返回但其目的是“副作用”的函数,例如 IO.puts/1
。在这些情况下,预期的返回类型是 :ok
。
相反,no_return()
应该用作永远不能返回值的函数的返回类型。这包括永远循环调用 receive
的函数,或者专门用于引发错误的函数,或者关闭 VM 的函数。