Spec-Zone.ru › Elixir 1.6

Спецификации типов

Elixir имеет обозначения для объявления типов и спецификаций. Elixir — язык с динамической типизацией, и поэтому спецификации типов никогда не используются компилятором для оптимизации или изменения кода. Тем не менее, использование спецификаций типов полезно, потому что

  • они предоставляют документацию (например, инструменты, такие как ExDoc, отображают спецификации типов в документации)
  • они используются инструментами, такими как Dialyzer, которые могут анализировать код со спецификациями типов для поиска несоответствий типов и возможных ошибок

Спецификации типов (иногда называемые typespecs) определяются в разных контекстах с помощью следующих атрибутов:

  • @type
  • @opaque
  • @typep
  • @spec
  • @callback
  • @macrocallback

Дополнительную информацию об определении типов и типовec можно найти в подразделах «Определение типа» и «Определение спецификации».

Типы и их синтаксис

Синтаксис спецификаций типов, предоставляемый Elixir, аналогичен синтаксису Erlang. Большинство встроенных типов, предоставляемых в Erlang (например, pid()) выражаются аналогично: pid() (или просто pid). Поддерживаются также параметризованные типы (например, list(integer)) и типы удаленных модулей (например, Enum.t). В качестве типов допускаются целые числа и атомные литералы (например, 1, :atom, или false). Все остальные типы построены из объединений предопределенных типов. Допускаются некоторые сокращения, такие как [...], <<>>, и {...}.

Основные типы

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()
      | 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>>

                                      ## Functions
      | (... -> type)                 # any arity, returns type
      | (() -> type)                  # 0-arity, returns type
      | (type1, type2 -> type)        # 2-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

Также можно определить переменные типа без ограничений.

@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.

Примечания

Elixir не рекомендует использовать тип t:string/0 , так как он может быть перепутат с бинарными данными, которые в Elixir называются «строками» (в отличие от списков символов). Для использования типа, который в Erlang называется t:string/0, необходимо использовать тип charlist/0, который является синонимом типа string. Если вы используете string, компилятор выдаст предупреждение.

Если вы хотите обратиться к типу «строка» (к тому, с которым работают функции в модуле String), используйте тип String.t/0 вместо него.

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

Spec-Zone.ru

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