Типы спецификаций
В Elixir есть обозначение для объявления типов и спецификаций. Elixir — язык с динамической типизацией, и поэтому спецификации типов никогда не используются компилятором для оптимизации или изменения кода. Тем не менее, использование спецификаций типов полезно, потому что:
- они предоставляют документацию (например, инструменты, такие как ExDoc, отображают спецификации типов в документации)
- они используются инструментами, такими как Dialyzer, которые могут анализировать код с типами спецификаций, чтобы найти несоответствия типов и возможные ошибки
Спецификации типов (иногда называемые типовыми спецификациями) определяются в разных контекстах с помощью следующих атрибутов:
@type@opaque@typep@spec@callback@macrocallback
Дополнительную информацию об определении типов и типовых спецификаций см. в разделах «Пользовательские типы» и «Определение спецификации» ниже.
Простой пример
defmodule StringHelpers do
@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());мы указываем, что функция
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(type1, type2) # proper or improper list
| nonempty_improper_list(type1, type2) # improper list
| nonempty_maybe_improper_list(type1, type2) # 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) # 0-arity, returns type
| (type1, type2 -> type) # 2-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 key :key of value_type
## Maps
| %{} # empty map
| %{key: value_type} # map with required key :key of 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>> |
bitstring() |
<<_::_*1>> |
boolean() |
false | true
|
byte() |
0..255 |
char() |
0..0x10FFFF |
charlist() |
[char()] |
nonempty_charlist() |
[char(), ...] |
fun() |
(... -> any) |
function() |
fun() |
identifier() |
reference() | port() | pid()
|
iodata() |
binary() | iolist()
|
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() |
float() | integer()
|
struct() |
%{:__struct__ => atom(), optional(atom()) => any()} |
timeout() |
non_neg_integer() | :infinity
|
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, любой перечисляемый объект может быть Enum.t/0 и так далее.
Карты
Ключевые типы в картах могут перекрываться, и в этом случае приоритет отдаётся левому ключу. Значение карты не принадлежит этому типу, если оно содержит ключ, который не входит в разрешённые ключи карты.
Если вы хотите указать, что ключи, которые не были ранее определены в карте, разрешены, обычно в конце типа карты указывается optional(any) => any.
Обратите внимание, что синтаксическое представление map() — это %{optional(any) => any}, а не %{}. Обозначение %{} указывает одиночный тип для пустой карты.
Пользовательские типы
Атрибуты модуля @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
Вы также можете давать имена аргументам в типовой спецификации, используя синтаксис 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_callback используются для создания функции 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 известны как «charlist». Они не относятся к Elixir-строкам, которые представляют собой двоичные данные, закодированные в UTF-8. Чтобы избежать путаницы, если вы попытаетесь использовать тип string(), Elixir выдаст предупреждение. Следует использовать charlist(), nonempty_charlist(), binary() или String.t() соответственно, или любое из нескольких буквальных представлений для этих типов.
Обратите внимание, что String.t() и binary() эквивалентны для инструментов анализа. Хотя для тех, кто читает документацию, String.t() подразумевает, что это двоичные данные, закодированные в UTF-8.
© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.10.4/typespecs.html