Spec-Zone.ru › Elixir 1.18

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

В 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] для списков, {type1, type2, ...} для кортежей и <<_ * _>> для двоичных данных.

Обозначение для представления объединения типов — вертикальная черта |. Например, тип 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>>
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 options [{:name, String.t} | {:max, pos_integer} | {:min, pos_integer}]

@type options [name: String.t, max: pos_integer, min: pos_integer]

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

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

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

Атрибуты @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 известны как «charlists». Они не относятся к строкам 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.36.1) для языка программирования Elixir

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

Spec-Zone.ru

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