非常教程

Elixir 1.5参考手册

指南 | Guide

类型规范和行为 | Typespecs and behaviours

类型和规格

Elixir 是一种动态类型语言,因此 Elixir 中的所有类型均由运行时推断。尽管如此,Elixir 附带了 typespecs,这是一种符号,用于:

  1. 声明类型化函数签名(规范);

2. 声明自定义数据类型。

功能规格

默认情况下,Elixir 提供了一些基本类型,比如integer或者pid更复杂的类型:例如,将 float 四舍五入到最接近的整数numberround/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/1call/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

请注意,您不需要定义行为以便在模块上进行动态分派,但这些功能常常并行不悖。

Elixir 1.5

Elixir 基于 Erlang 虚拟机的函数式、面向并行,是一种较好的编程语言。它以 Erlang 为基础,支持分布式、高容错、实时应用程序的开发。

主页 https://elixir-lang.org/
源码 https://github.com/elixir-lang/elixir
版本 1.5
发布版本 1.5.2