Spec-Zone.ru › Elixir 1.16

Исходный код Справочник по типам

В Elixir есть обозначения для объявления типов и спецификаций. Этот документ служит справочником по их использованию и синтаксису.

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

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

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

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

Кроме того, вы можете использовать @typedoc для документирования определения пользовательского @type.

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

Простой пример

defmodule StringHelpers do
  @typedoc "A word from the dictionary"
  @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()).

  • Мы описываем тип с помощью @typedoc, который будет включён в сгенерированную документацию.

  • Мы указываем, что функция 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. Это обычно называется типом «сумма» (sum type) в других языках.

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

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

Список ключевых слов

Помимо keyword() и keyword(t), может быть полезно составить спецификацию для ожидаемого списка ключевых слов. Например:

@type option :: {:name, String.t} | {:max, pos_integer} | {:min, pos_integer}
@type options :: [option()]

Это ясно показывает, что только эти опции разрешены, ни одна не является обязательной, и порядок не имеет значения.

Также это позволяет комбинировать с существующими типами. Например:

type option :: {:my_option, String.t()} | GenServer.option()

@spec start_link([option()]) :: GenServer.on_start()
def start_link(opts) do
  {my_opts, gen_server_opts} = Keyword.split(opts, [:my_option])
  GenServer.start_link(__MODULE__, my_opts, gen_server_opts)
end

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

Атрибуты модуля @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 и функции абстрагируют всю передачу сообщений (отправку и получение) и сообщения об ошибках, которые, вероятно, захочет реализовать процесс «сервера», от конкретных частей, таких как действия, которые должен выполнить этот процесс сервера.

Предположим, мы хотим реализовать набор парсеров, каждый из которых парсит структурированные данные: например, парсер JSON и парсер MessagePack. Каждый из этих двух парсеров будет вести себя одинаково: оба будут предоставлять функцию parse/1 и функцию extensions/0. Функция parse/1 вернёт представление структурированных данных в Elixir, а функция extensions/0 вернёт список расширений файлов, которые могут быть использованы для каждого типа данных (например, .json для файлов JSON).

Мы можем создать поведение Parser:

defmodule Parser do
  @doc """
  Parses a string.
  """
  @callback parse(String.t) :: {:ok, term} | {:error, atom}

  @doc """
  Lists all supported file extensions.
  """
  @callback extensions() :: [String.t]
end

Как видно из примера выше, определение обратного вызова сводится к определению спецификации для этого обратного вызова, состоящей из:

  • имени обратного вызова (parse или extensions в примере)
  • аргументов, которые должен принимать обратный вызов (String.t)
  • ожидаемого типа возвращаемого значения обратного вызова

Модули, использующие поведение Parser, должны реализовать все функции, определённые с помощью атрибута @callback. Как видно, @callback ожидает имя функции, а также спецификацию функции, подобную тем, которые используются с атрибутом @spec.

Реализация поведения

Реализация поведения проста:

defmodule JSONParser do
  @behaviour Parser

  @impl Parser
  def parse(str), do: {:ok, "some json " <> str} # ... parse JSON

  @impl Parser
  def extensions, do: [".json"]
end
defmodule CSVParser do
  @behaviour Parser

  @impl Parser
  def parse(str), do: {:ok, "some csv " <> str} # ... parse CSV

  @impl Parser
  def extensions, do: [".csv"]
end

Если модуль, использующий данное поведение, не реализует один из обратных вызовов, необходимых для этого поведения, будет сгенерировано предупреждение во время компиляции.

Кроме того, с помощью @impl вы также можете убедиться, что вы реализуете правильные обратные вызовы из данного поведения явным образом. Например, следующий парсер реализует как parse, так и extensions. Однако, благодаря опечатке, BADParser реализует parse/0 вместо parse/1.

defmodule BADParser do
  @behaviour Parser

  @impl Parser
  def parse, do: {:ok, "something bad"}

  @impl Parser
  def extensions, do: ["bad"]
end

Этот код генерирует предупреждение, информируя вас о том, что вы ошибочно реализуете parse/0 вместо parse/1 . Дополнительную информацию о @impl вы можете найти в документации по модулям.

Использование поведения

Поведение полезно, потому что вы можете передавать модули в качестве аргументов и затем вызывать любые функции, указанные в поведении. Например, мы можем иметь функцию, которая получает имя файла, несколько парсеров и парсит файл в зависимости от его расширения:

@spec parse_path(Path.t(), [module()]) :: {:ok, term} | {:error, atom}
def parse_path(filename, parsers) do
  with {:ok, ext} <- parse_extension(filename),
       {:ok, parser} <- find_parser(ext, parsers),
       {:ok, contents} <- File.read(filename) do
    parser.parse(contents)
  end
end

defp parse_extension(filename) do
  if ext = Path.extname(filename) do
    {:ok, ext}
  else
    {:error, :no_extension}
  end
end

defp find_parser(ext, parsers) do
  if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do
    {:ok, parser}
  else
    {:error, :no_matching_parser}
  end
end

Вы также можете вызвать любой парсер напрямую: CSVParser.parse(...).

Обратите внимание, что вам не нужно определять поведение, чтобы динамически выполнять диспетчеризацию по модулю, но эти функции часто идут рука об руку.

Необязательные обратные вызовы

Необязательные обратные вызовы — это обратные вызовы, которые модули-обработчики могут реализовать по желанию, но не обязаны. Обычно модули поведения знают, нужно ли им вызывать эти обратные вызовы, исходя из конфигурации, или проверяют, определены ли обратные вызовы с помощью 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.

Проверка поведения

Атрибуты @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.

Функции, которые вызывают ошибку

Спецификации типов не должны указывать, что функция может вызвать ошибку; любая функция может потерпеть неудачу в любое время, если ей будет предоставлен неверный ввод. В прошлом стандартная библиотека Elixir иногда использовала no_return() для указания этого, но эти случаи были устранены.

Тип no_return() также не следует использовать для функций, которые возвращают значение, но предназначены для «побочного эффекта», например, IO.puts/1. В этих случаях ожидаемым типом возврата является :ok.

Вместо этого, no_return() следует использовать в качестве типа возврата для функций, которые никогда не могут вернуть значение. Это включает функции, которые циклично вызывают receive, или которые существуют специально для вызова ошибки, или которые завершают работу виртуальной машины.

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

Скачать версию ePub

Создано с помощью ExDoc (v0.32.2) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/typespecs.html

Spec-Zone.ru

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