Источник Справочник по типам спецификаций
Elixir имеет обозначение для объявления типов и спецификаций. Этот документ является справочником по их использованию и синтаксису.
Elixir — язык с динамической типизацией, и поэтому спецификации типов никогда не используются компилятором для оптимизации или изменения кода. Тем не менее, использование спецификаций типов полезно, потому что:
- они предоставляют документацию (например, такие инструменты, как
ExDocпоказывают спецификации типов в документации) - они используются такими инструментами, как Dialyzer, которые могут анализировать код со спецификациями типов для поиска несоответствий типов и возможных ошибок
Спецификации типов (чаще всего называемые типовыми спецификациями) определяются в различных контекстах с помощью следующих атрибутов:
@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] для списков, {type1, type2, ...} для кортежей и <<_ * _>> для бинарных данных.
Обозначение для представления объединения типов — это символ "|" |. Например, типовая спецификация type :: atom() | pid() | tuple() создаёт тип type, который может быть либо atom, либо pid, либо tuple. Это обычно называется типом «сумма» в других языках.
Основные типы
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 options [{:name, String.t} | {:max, pos_integer} | {:min, pos_integer}]
@type options [name: String.t, max: pos_integer, min: pos_integer]
Пользовательские типы
Атрибуты модуля @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/1.
Проверка поведения
Атрибуты @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 строкам, которые представляют собой 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, или функции, которые существуют специально для вызова ошибки, или функции, которые завершают работу виртуальной машины.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/typespecs.html