Типы и спецификации
В 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()
| struct() # any struct
| 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() |
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()
|
Удалённые типы
Любой модуль также может определять свои собственные типы, и модули 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.
Тип string()
Elixir не рекомендует использовать тип string(). Тип string() относится к строкам Erlang, которые в Elixir известны как «charlists». Они не относятся к строкам Elixir, которые представляют собой двоичные данные, закодированные в UTF-8. Чтобы избежать путаницы, если вы попытаетесь использовать тип string(), Elixir выведет предупреждение. Следует использовать 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.8.2/typespecs.html