Spec-Zone.ru › Elixir 1.13

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

В 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)                     # 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
      | %{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, любой перечисляемый объект может быть 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_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.

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

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

Spec-Zone.ru

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