Spec-Zone.ru › Elixir 1.14

Типы

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(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>>
bitstring() <<_::_*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}, а не %{}. Обозначение %{} определяет одиночный тип для пустой карты.

Пользовательские типы

Атрибуты модуля @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.

← Предыдущая страница Справочник по синтаксису
Следующая страница → Синтаксис Юникода

© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.14.1/typespecs.html

Spec-Zone.ru

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