Spec-Zone.ru › Elixir 1.8

Типы и спецификации

В 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API