Типы спецификаций
В 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 :: 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, @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 и функции абстрагируют весь обмен сообщениями (отправка и получение) и сообщения об ошибках, которые, вероятно, захочет реализовать процесс «сервера», от специфических частей, таких как действия, которые должен выполнять этот процесс сервера.
Для определения модуля поведения достаточно определить один или несколько обратных вызовов в этом модуле. Для определения обратных вызовов можно использовать атрибуты модуля @callback и @macrocallback (соответственно для функций обратных вызовов и макросов обратных вызовов).
defmodule MyBehaviour do @callback my_fun(arg :: any) :: any @macrocallback my_macro(arg :: any) :: Macro.t end
Как видно из примера выше, определение обратного вызова заключается в определении спецификации этого обратного вызова, состоящей из:
- названия обратного вызова (
my_funилиmy_macroв примере) - аргументов, которые должен принимать обратный вызов (
arg :: anyв примере) - ожидаемого типа возвращаемого значения обратного вызова
Необязательные обратные вызовы
Необязательные обратные вызовы — это обратные вызовы, которые модули обратного вызова могут реализовать, если захотят, но не обязаны. Обычно модули поведения знают, должны ли они вызывать эти обратные вызовы, основываясь на конфигурации, или они проверяют, определены ли обратные вызовы с помощью 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.
Реализация поведений
Чтобы указать, что модуль реализует данное поведение, необходимо использовать атрибут @behaviour:
defmodule MyBehaviour do @callback my_fun(arg :: any) :: any end defmodule MyCallbackModule do @behaviour MyBehaviour def my_fun(arg), do: arg end
Если модуль обратного вызова, реализующий данное поведение, не экспортирует все функции и макросы, определенные этим поведением, пользователю будет сообщено об этом с помощью предупреждений во время процесса компиляции (ошибки не произойдут).
Стандартная библиотека Elixir содержит несколько часто используемых поведений, таких как GenServer, Supervisor и Application.
Просмотр поведений
Атрибуты @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 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.15.4/typespecs.html