Spec-Zone.ru › Elixir 1.14

Kernel.SpecialForms

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

Модуль Kernel.SpecialForms состоит только из макросов, которые можно вызывать в любом коде Elixir без использования префикса Kernel.SpecialForms.. Это возможно, потому что все они были автоматически импортированы, так же, как функции и макросы из модуля Kernel.

В этом модуле определены эти строительные блоки. Некоторые из этих специальных форм являются лексическими (например, alias/2 и case/2). Макросы {}/1 и <<>>/1 также являются специальными формами, используемыми для определения кортежей и бинарных структур данных соответственно.

В этом модуле также документируются макросы, возвращающие информацию об среде компиляции Elixir, такие как (__ENV__/0, __MODULE__/0, __DIR__/0, __STACKTRACE__/0 и __CALLER__/0).

Кроме того, он документирует две специальные формы, __block__/1 и __aliases__/1, которые не предназначены для прямого вызова разработчиком, но они появляются в цитируемом содержимом, поскольку они являются необходимыми элементами конструкций Elixir.

Краткое описание

Функции

%struct{}

Сопоставление со структурой или создание структуры.

%{}

Создаёт карту.

&expr

Оператор захвата. Захватывает или создаёт анонимную функцию.

left . right

Оператор точки. Определяет удалённый вызов, вызов анонимной функции или псевдоним.

__aliases__(args)

Внутренняя специальная форма для хранения информации о псевдонимах.

__block__(args)

Внутренняя специальная форма для блочных выражений.

__CALLER__

Возвращает текущую среду вызова как структуру Macro.Env.

__DIR__

Возвращает абсолютный путь к каталогу текущего файла в виде бинарного значения.

__ENV__

Возвращает текущую информацию об окружении как структуру Macro.Env.

__MODULE__

Возвращает имя текущего модуля в виде атома или nil в противном случае.

__STACKTRACE__

Возвращает стек вызовов для текущего обработанного исключения.

left :: right

Оператор типа. Используется типами и битовыми строками для указания типов.

<<args>>

Определяет новую битовую строку.

left = right

Оператор сопоставления. Сопоставляет значение справа с шаблоном слева.

alias(module, opts)

alias/2 используется для создания псевдонимов, что часто полезно с именами модулей.

case(condition, clauses)

Сопоставляет данное выражение с данными разделами.

cond(clauses)

Вычисляет выражение, соответствующее первому разделу, который вычисляется как истинное значение.

fn(clauses)

Определяет анонимную функцию.

for(args)

Понимания позволяют быстро создать структуру данных из перечисляемого или битовой строки.

import(module, opts)

Импортирует функции и макросы из других модулей.

quote(opts, block)

Получает представление любого выражения.

receive(args)

Проверяет, есть ли сообщение, соответствующее заданным разделам, в почтовом ящике текущего процесса.

require(module, opts)

Требует модуль для использования его макросов.

super(args)

Вызывает переопределённую функцию при её переопределении с помощью Kernel.defoverridable/1.

try(args)

Вычисляет заданные выражения и обрабатывает любые ошибки, завершения или броски, которые могли произойти.

unquote(expr)

Убирает кавычки из данного выражения внутри цитируемого выражения.

unquote_splicing(expr)

Убирает кавычки из данного списка, расширяя его аргументы.

with(args)

Используется для объединения совпадающих разделов.

^var

Оператор привязки. Доступ к уже связанной переменной в разделах сопоставления.

{args}

Создаёт кортеж.

END_OF_DOCUMENT_MARKER

Функции

%struct{}Source

Сопоставление со структурой или создание структуры.

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

Структуры обычно определяются с помощью макроса Kernel.defstruct/1:

defmodule User do
  defstruct name: "john", age: 27
end

Теперь структуру можно создать следующим образом:

%User{}

Под капотом структура — это просто карта с ключом :__struct__, указывающим на модуль User:

%User{} == %{__struct__: User, name: "john", age: 27}

Поля структуры можно указать при создании структуры:

%User{age: 31}
#=> %{__struct__: User, name: "john", age: 31}

Или также при сопоставлении с образцом для извлечения значений:

%User{age: age} = user

Также доступна операция обновления, специфичная для структур:

%User{user | age: 28}

Преимущество структур заключается в том, что они проверяют, являются ли заданные ключи частью определённой структуры. Пример ниже завершится ошибкой, так как нет ключа :full_name в структуре User:

%User{full_name: "john doe"}

Указанный выше синтаксис гарантирует, что заданные ключи являются допустимыми на этапе компиляции, и гарантирует на этапе выполнения, что переданный аргумент является структурой, в противном случае произойдёт ошибка BadStructError.

Хотя структуры являются картами, по умолчанию структуры не реализуют ни один из протоколов, реализованных для карт. Дополнительную информацию о том, как структуры могут использоваться с протоколами для полиморфного диспетчера, см. в Kernel.defprotocol/2. Также см. Kernel.struct/2 и Kernel.struct!/2 для примеров создания и обновления структур динамически.

Сопоставление с образцом по именам структур

Помимо сопоставления с образцом по полям структур, например:

%User{age: age} = user

Структуры также позволяют сопоставлять с образцом по имени структуры:

%struct_name{} = user
struct_name #=> User

Вы также можете назначить имя структуры _ когда вы хотите проверить, является ли что-то структурой, но вас не интересует её имя:

%_{} = user

%{}Source

Создаёт карту.

См. модуль Map для получения дополнительной информации о картах, их синтаксисе и способах доступа к ним и манипулирования ими.

Представление AST

Независимо от того, используется ли => или синтаксис с ключевыми словами, пары «ключ-значение» в картах всегда представляются внутри как список кортежей из двух элементов для простоты:

iex> quote do
...>   %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]}

&exprSource

Оператор захвата. Захватывает или создаёт анонимную функцию.

Захват

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

iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false

В примере выше мы захватили Kernel.is_atom/1 как анонимную функцию, а затем вызвали её.

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

&local_function/1

См. также Function.capture/3.

Анонимные функции

Оператор захвата также может использоваться для частичного применения функций, где &1, &2 и так далее могут использоваться в качестве заполнительных значений. Например:

iex> double = &(&1 * 2)
iex> double.(2)
4

Другими словами, &(&1 * 2) эквивалентно fn x -> x * 2 end.

Мы можем частично применить удалённую функцию с заполнителем:

iex> take_five = &Enum.take(&1, 5)
iex> take_five.(1..10)
[1, 2, 3, 4, 5]

Ещё один пример с использованием импортированной или локальной функции:

iex> first_elem = &elem(&1, 0)
iex> first_elem.({0, 1})
0

Оператор & может использоваться с более сложными выражениями:

iex> fun = &(&1 + &2 + &3)
iex> fun.(1, 2, 3)
6

А также со списками и кортежами:

iex> fun = &{&1, &2}
iex> fun.(1, 2)
{1, 2}

iex> fun = &[&1 | &2]
iex> fun.(1, [2, 3])
[1, 2, 3]

Единственные ограничения при создании анонимных функций — наличие хотя бы одного заполнителя, т.е. он должен содержать хотя бы &1, и не поддерживаются выражения блоков:

# No placeholder, fails to compile.
&(:foo)

# Block expression, fails to compile.
&(&1; &2)

left . rightSource

Оператор точки. Определяет удалённый вызов, вызов анонимной функции или псевдоним.

Точка (.) в Elixir может использоваться для удалённых вызовов:

iex> String.downcase("FOO")
"foo"

В данном примере мы использовали . для вызова downcase в модуле String, передавая "FOO" в качестве аргумента.

Точка может использоваться и для вызова анонимных функций:

iex> (fn n -> n end).(7)
7

в этом случае слева есть функция.

Мы также можем использовать точку для создания псевдонимов:

iex> Hello.World
Hello.World

На этот раз мы объединили два псевдонима, определив конечный псевдоним Hello.World.

Синтаксис

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

iex> Kernel.Sample
Kernel.Sample

iex> Kernel.length([1, 2, 3])
3

iex> Kernel.+(1, 2)
3

iex> Kernel."+"(1, 2)
3

Использование одинарных или двойных кавычек для имени функции всегда представляет удалённый вызов. Поэтому Kernel."Foo" попытается вызвать функцию «Foo», а не вернуть псевдоним Kernel.Foo. Это сделано по дизайну, так как имена модулей более строгие, чем имена функций.

При использовании точки для вызова анонимной функции есть только один операнд, но он всё равно записывается с использованием постфиксной нотации:

iex> negate = fn n -> -n end
iex> negate.(7)
-7

Выражение в кавычках

Когда . используется, выражение в кавычках может принимать две разные формы. Когда правая часть начинается со строчной буквы (или нижнего подчёркивания):

iex> quote do
...>   String.downcase("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

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

{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}

Этот кортеж следует общей структуре выражения в кавычках в Elixir: имя как первый аргумент, какой-то список ключевых слов как метаданные как второй и список аргументов как третий. В этом случае аргументы — псевдоним String и атом :downcase. Второй аргумент в удалённом вызове всегда является атомом.

В случае вызовов анонимных функций внутренний кортеж со специальной формой точки имеет только один аргумент, отражающий тот факт, что оператор является унарным:

iex> quote do
...>   negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}

В случае, когда правая часть — псевдоним (т.е. начинается с заглавной буквы), мы получаем вместо этого:

iex> quote do
...>   Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}

Более подробную информацию о псевдонимах см. в документации по специальной форме __aliases__/1.

Разъяснение

Мы также можем использовать разъяснение для генерации удалённого вызова в выражении в кавычках:

iex> x = :downcase
iex> quote do
...>   String.unquote(x)("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

Подобно Kernel."FUNCTION_NAME", unquote(x) всегда будет генерировать удалённый вызов, независимо от значения x. Для генерации псевдонима через выражение в кавычках необходимо использовать Module.concat/2:

iex> x = Sample
iex> quote do
...>   Module.concat(String, unquote(x))
...> end
{{:., [], [{:__aliases__, [alias: false], [:Module]}, :concat]}, [],
 [{:__aliases__, [alias: false], [:String]}, Sample]}

__aliases__(args)Source

Внутренняя специальная форма для хранения информации о псевдонимах.

Обычно она компилируется в атом:

iex> quote do
...>   Foo.Bar
...> end
{:__aliases__, [alias: false], [:Foo, :Bar]}

Elixir представляет Foo.Bar как __aliases__ для однозначной идентификации вызовов оператором :.. Например:

iex> quote do
...>   Foo.bar()
...> end
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}

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

С другой стороны, псевдонимы содержат некоторые свойства:

  1. Первый элемент псевдонимов может быть любым элементом, который должен быть преобразован в атом на этапе компиляции.

  2. Элементы псевдонимов, следующие за первым, всегда гарантированно являются атомами.

  3. Если первый элемент псевдонима — атом :Elixir, преобразование не происходит.

__block__(args)Source

Внутренняя специальная форма для выражений блоков.

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

iex> quote do
...>   1
...>   2
...>   3
...> end
{:__block__, [], [1, 2, 3]}

__CALLER__Source

Возвращает текущую среду вызова как структуру Macro.Env.

В среде вы можете получить доступ к имени файла, номерам строк, настроить псевдонимы, функцию и другие данные.

__DIR__Source

Возвращает абсолютный путь к директории текущего файла в виде двоичной строки.

Хотя к директории можно получить доступ как Path.dirname(__ENV__.file), этот макрос является удобным сокращением.

__ENV__Source

Возвращает текущую информацию об окружающей среде как структуру Macro.Env.

В среде вы можете получить доступ к текущему имени файла, номерам строк, установить псевдонимы, текущую функцию и другие данные.

END_OF_DOCUMENT_MARKER

__MODULE__Source

Возвращает текущее имя модуля как атом или nil в противном случае.

Хотя к модулю можно получить доступ в __ENV__/0, эта макрокоманда является удобным сокращением.

__STACKTRACE__Source

Возвращает трассировку стека для обрабатываемой в данный момент исключительной ситуации.

Доступна только в catch и rescue разделах выражений try/1.

Для получения трассировки стека текущего процесса используйте Process.info(self(), :current_stacktrace) вместо этого.

left :: rightSource

Оператор типа. Используется типами и битовыми строками для указания типов.

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

@type number :: integer | float
@spec add(number, number) :: number

Он также может использоваться в битовых строках для указания типа заданного битового сегмента:

<<int::integer-little, rest::bits>> = bits

Прочитайте документацию на странице Описания типов и <<>>/1 для получения дополнительной информации о типах и битовых строках соответственно.

<<args>>Source

Определяет новую битовую строку.

Примеры

iex> <<1, 2, 3>>
<<1, 2, 3>>

Типы

Битовая строка состоит из многих сегментов, и каждый сегмент имеет тип. В битовых строках используются 9 типов:

  • integer
  • float
  • bits (псевдоним для bitstring)
  • bitstring
  • binary
  • bytes (псевдоним для binary)
  • utf8
  • utf16
  • utf32

Если тип не указан, по умолчанию используется integer:

iex> <<1, 2, 3>>
<<1, 2, 3>>

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

iex> <<0, "foo">>
<<0, 102, 111, 111>>

Бинарные данные необходимо явно пометить как binary:

iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"

Типы utf8, utf16, и utf32 предназначены для кодов Unicode. Они также могут применяться к литеральным строкам и списку символов:

iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>
iex> <<"foo"::utf32>>
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>

В противном случае при построении бинарного объекта возникает ArgumentError:

rest = "oo"
<<102, rest>>
** (ArgumentError) argument error

Параметры

Многие параметры можно задать, используя - в качестве разделителя. Порядок произвольный, поэтому следующие варианты эквивалентны:

<<102::integer-native, rest::binary>>
<<102::native-integer, rest::binary>>
<<102::unsigned-big-integer, rest::binary>>
<<102::unsigned-big-integer-size(8), rest::binary>>
<<102::unsigned-big-integer-8, rest::binary>>
<<102::8-integer-big-unsigned, rest::binary>>
<<102, rest::binary>>

Единица и размер

Длина совпадения равна unit (количество бит) умноженному на size (количество повторяющихся сегментов длиной unit).

Тип Единица по умолчанию
integer 1 бит
float 1 бит
binary 8 бит

Размеры типов немного сложнее. Размер целых чисел по умолчанию равен 8.

Для чисел с плавающей точкой он равен 64. Для чисел с плавающей точкой size * unit должно равняться 16, 32 или 64, что соответствует IEEE 754 binary16, binary32 и binary64 соответственно.

Для бинарных данных по умолчанию используется размер бинарных данных. Только последний бинарный объект в совпадении может использовать размер по умолчанию. Все остальные должны иметь явно указанный размер, даже если совпадение однозначное. Например:

iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}

Размер может быть переменной или любым допустимым выражением с условием:

iex> name_size = 5
iex> <<name::binary-size(name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}

Размер может получать доступ к предыдущим переменным, определённым в самом бинарном объекте/битовой строке:

iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}

Однако он не может получать доступ к переменным, определённым в совпадении за пределами бинарного объекта/битовой строки:

{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment

Если размер не указан для объекта, не являющегося последним, компиляция завершится ошибкой:

<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">>
** (CompileError): a binary field without size is only allowed at the end of a binary pattern

Сокращенная запись

Размер и единицу также можно задать, используя сокращенную запись при передаче целочисленных значений:

iex> x = 1
iex> <<x::8>> == <<x::size(8)>>
true
iex> <<x::8*4>> == <<x::size(8)-unit(4)>>
true

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

Модификаторы

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

Модификатор Соответствующий тип(ы)
signed integer
unsigned (по умолчанию) integer
little integer, float, utf16, utf32
big (по умолчанию) integer, float, utf16, utf32
native integer, utf16, utf32

Знак

Целые числа могут быть signed или unsigned, по умолчанию unsigned.

iex> <<int::integer>> = <<-100>>
<<156>>
iex> int
156
iex> <<int::integer-signed>> = <<-100>>
<<156>>
iex> int
-100

signed и unsigned используются только для сопоставления бинарных данных (см. ниже) и только для целых чисел.

iex> <<-100::signed, _rest::binary>> = <<-100, "foo">>
<<156, 102, 111, 111>>

Порядок байтов

В Elixir есть три варианта порядка байтов: big, little, и native. По умолчанию big:

iex> <<number::little-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
256
iex> <<number::big-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
1

native определяется виртуальной машиной при запуске и зависит от операционной системы хоста.

Сопоставление бинарных/битовых строк

Сопоставление бинарных данных — мощная функция в Elixir, полезная для извлечения информации из бинарных данных, а также для сопоставления шаблонов.

Сопоставление бинарных данных может использоваться само по себе для извлечения информации из бинарных данных:

iex> <<"Hello, ", place::binary>> = "Hello, World"
"Hello, World"
iex> place
"World"

Или как часть определений функций для сопоставления шаблонов:

defmodule ImageTyper do
  @png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
                   13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
  @jpg_signature <<255::size(8), 216::size(8)>>

  def type(<<@png_signature, _rest::binary>>), do: :png
  def type(<<@jpg_signature, _rest::binary>>), do: :jpg
  def type(_), do: :unknown
end

Производительность и оптимизации

Компилятор Erlang может обеспечить ряд оптимизаций при создании и сопоставлении бинарных данных. Чтобы увидеть вывод оптимизации, установите параметр компилятора bin_opt_info:

ERL_COMPILER_OPTIONS=bin_opt_info mix compile

Для получения дополнительной информации о конкретных оптимизациях и соображениях по производительности ознакомьтесь с главой "Constructing and matching binaries" в руководстве по производительности Erlang.

left = rightSource

Оператор совпадения. Сопоставляет значение справа с шаблоном слева.

alias(module, opts)Source

alias/2 используется для создания псевдонимов, что часто бывает полезно с именами модулей.

Примеры

alias/2 можно использовать для создания псевдонима любого модуля:

defmodule Math do
  alias MyKeyword, as: Keyword
end

В примере выше мы создали псевдоним MyKeyword как Keyword. Теперь любая ссылка на Keyword будет автоматически заменена на MyKeyword.

В случае необходимости доступа к исходному Keyword, это можно сделать, обратившись к Elixir:

Keyword.values #=> uses MyKeyword.values
Elixir.Keyword.values #=> uses Keyword.values

Обратите внимание, что вызов alias без опции :as автоматически создаёт псевдоним на основе последней части модуля. Например:

alias Foo.Bar.Baz

Это то же самое, что:

alias Foo.Bar.Baz, as: Baz

Мы также можем создать псевдонимы для нескольких модулей в одной строке:

alias Foo.{Bar, Baz, Biz}

Это то же самое, что:

alias Foo.Bar
alias Foo.Baz
alias Foo.Biz

Лексическая область видимости

import/2, require/2 и alias/2 являются директивами и все обладают лексической областью видимости. Это означает, что вы можете создавать псевдонимы внутри определённых функций, и это не повлияет на общую область видимости.

Предупреждения

Если вы создаёте псевдоним для модуля и не используете его, Elixir выведет предупреждение, что псевдоним не используется.

Если псевдоним генерируется автоматически макросом, Elixir не выдаст никаких предупреждений, так как псевдоним не был явно определён.

Оба типа поведения предупреждений можно изменить, явно задав параметр :warn со значением true или false.

case(condition, clauses)Source

Сопоставляет заданное выражение с заданными условиями.

Примеры

case File.read(file) do
  {:ok, contents} when is_binary(contents) ->
    String.split(contents, "\n")

  {:error, _reason} ->
    Logger.warning "could not find #{file}, assuming empty..."
    []
end

В примере выше, мы сопоставляем результат File.read/1 с каждой частью условия ("head") и выполняем часть кода ("body"), соответствующую первой подходящей части.

Если ни одна часть не соответствует, возникает ошибка. По этой причине может потребоваться добавить заключительную часть (например, _), которая всегда будет соответствовать.

x = 10

case x do
  0 ->
    "This clause won't match"

  _ ->
    "This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"

Обработка переменных

Обратите внимание, что переменные, связанные в части условия, не "вытекают" во внешнюю область видимости:

case data do
  {:ok, value} -> value
  :error -> nil
end

value
#=> unbound variable value

Переменные во внешней области видимости также не могут быть перезаписаны:

value = 7

case lucky? do
  false -> value = 13
  true -> true
end

value
#=> 7

В примере выше, value будет 7 независимо от значения lucky?. Переменная value связанная в части условия и переменная value из внешней области видимости — это две совершенно разные переменные.

Если необходимо сопоставить шаблон с уже существующей переменной, необходимо использовать оператор ^/1.

x = 1

case 10 do
  ^x -> "Won't match"
  _ -> "Will match"
end
#=> "Will match"

Использование защитных условий для сопоставления с несколькими значениями

Хотя невозможно сопоставить несколько шаблонов в одном условии, можно сопоставить несколько значений с помощью защитных условий:

case data do
  value when value in [:one, :two] ->
    "#{value} has been matched"

  :three ->
    "three has been matched"
end

cond(clauses)Source

Вычисляет выражение, соответствующее первому условию, которое вычисляется как истинное.

cond do
  hd([1, 2, 3]) ->
    "1 is considered as true"
end
#=> "1 is considered as true"

Возникает ошибка, если все условия вычисляются как nil или false. По этой причине может потребоваться добавить последнее условие, которое всегда будет истинным (любое не-false и не-nil), которое всегда будет соответствовать.

Примеры

cond do
  1 + 1 == 1 ->
    "This will never match"
  2 * 2 != 4 ->
    "Nor this"
  true ->
    "This will"
end
#=> "This will"

fn(clauses)Source

Определяет анонимную функцию.

См. Function для получения дополнительной информации.

Примеры

iex> add = fn a, b -> a + b end
iex> add.(1, 2)
3

Анонимные функции также могут иметь несколько частей. Все части должны ожидать одинаковое количество аргументов:

iex> negate = fn
...>   true -> false
...>   false -> true
...> end
iex> negate.(false)
true

for(args)Source

Понимания позволяют быстро создавать структуру данных из перечисляемого объекта или битового массива.

Давайте начнём с примера:

iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

Понимание принимает много генераторов и фильтров. Перечисляемые генераторы определяются с помощью <-:

# A list generator:
iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]

Фильтры также могут быть заданы:

# A comprehension with a generator and a filter
iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
[2, 4, 6]

Фильтры должны вычисляться как истинные значения (все, кроме nil и false). Если фильтр ложный, то текущее значение отбрасывается.

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

iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...>   String.upcase(name)
...> end
["JOHN", "MEG"]

Также поддерживаются генераторы битовых массивов, которые очень полезны при работе с потоками битовых массивов:

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

Присваивания переменных внутри понимания, будь то в генераторах, фильтрах или внутри блока, не отражаются за пределами понимания.

Присваивания переменных внутри фильтров по-прежнему должны возвращать истинное значение, в противном случае значения отбрасываются. Давайте рассмотрим пример. Представьте себе список ключевых слов, где ключ — это язык программирования, а значение — его непосредственный родитель. Затем давайте попробуем вычислить прародителя каждого языка. Вы могли бы попробовать это:

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent = languages[parent], do: {language, grandparent}
[elixir: :prolog]

Учитывая, что прародители Erlang и Prolog были nil, эти значения были отфильтрованы. Если вы не хотите такого поведения, простым вариантом является перемещение фильтра внутрь блока do:

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages do
...>   grandparent = languages[parent]
...>   {language, grandparent}
...> end
[elixir: :prolog, erlang: nil, prolog: nil]

Однако такой вариант не всегда доступен, так как могут быть дополнительные фильтры. Альтернативой является преобразование фильтра в генератор, обернув правую часть = в список:

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent <- [languages[parent]], do: {language, grandparent}
[elixir: :prolog, erlang: nil, prolog: nil]

Опции :into и :uniq

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

Например, мы можем использовать генераторы битовых массивов с опцией :into для легкого удаления всех пробелов в строке:

iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

Модуль IO предоставляет потоки, которые являются как Enumerable, так и Collectable, вот сервер эхо-ответа с использованием понимания, преобразующий все в верхний регистр:

for line <- IO.stream(), into: IO.stream() do
  String.upcase(line)
end

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

iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]

iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>>
"ABC"

Опция :reduce

Хотя опция :into позволяет настроить поведение понимания для заданного типа данных, например, поместить все значения в карту или в двоичное представление, этого не всегда достаточно.

Например, представьте себе двоичную строку с буквами, где вы хотите посчитать, сколько раз встречается каждая строчная буква, игнорируя все заглавные. Например, для строки "AbCabCABc", мы хотим вернуть карту %{"a" => 1, "b" => 2, "c" => 1}.

Если мы будем использовать :into, нам понадобится тип данных, который вычисляет частоту каждого элемента, который он содержит. Хотя такого типа данных в Elixir нет, вы можете реализовать его самостоятельно.

Более простым вариантом было бы использовать понимания для сопоставления и фильтрации букв, а затем вызвать Enum.reduce/3 для построения карты, например:

iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}

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

К счастью, понимания также поддерживают опцию :reduce, которая позволит нам объединить оба шага в один шаг:

iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...>   acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}

Когда задан ключ :reduce, его значение используется в качестве начального накопителя, и блок do должен быть изменён на использование -> условий, где левая часть -> получает накопленное значение предыдущей итерации, а выражение справа должно возвращать новое значение накопителя. После того, как все элементы обработаны, возвращается конечное накопленное значение. Если элементов нет, возвращается начальное значение накопителя.

import(module, opts)Source

Импортирует функции и макросы из других модулей.

import/2 позволяет легко получить доступ к функциям или макросам из других модулей без использования квалифицированного имени.

Примеры

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

iex> import List
iex> flatten([1, [2], 3])
[1, 2, 3]

Выбор

По умолчанию Elixir импортирует функции и макросы из данного модуля, за исключением тех, которые начинаются с символа подчёркивания (обычно это коллбэки):

import List

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

import List, only: :functions
import List, only: :macros
import Kernel, only: :sigils

В качестве альтернативы, Elixir позволяет разработчику передавать пары имя/арность в :only или :except для точного управления тем, что импортировать (или нет):

import List, only: [flatten: 1]
import String, except: [split: 2]

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

import List, only: [flatten: 1, keyfind: 4]
import List, except: [flatten: 1]

После двух вызовов импорта будет импортирована только функция List.keyfind/4.

Функции с символом подчёркивания

По умолчанию функции, начинающиеся с _ , не импортируются. Если вам действительно нужно импортировать функцию, начинающуюся с _, вы должны явно включить её в селектор :only.

import File.Stream, only: [__build__: 3]

Лексическая область видимости

Важно отметить, что import/2 является лексическим. Это означает, что вы можете импортировать определённые макросы внутри конкретных функций:

defmodule Math do
  def some_function do
    # 1) Disable "if/2" from Kernel
    import Kernel, except: [if: 2]

    # 2) Require the new "if/2" macro from MyMacros
    import MyMacros

    # 3) Use the new macro
    if do_something, it_works
  end
end

В примере выше мы импортировали макросы из MyMacros, заменив оригинальную реализацию if/2 нашей собственной внутри этой конкретной функции. Все остальные функции в этом модуле по-прежнему смогут использовать оригинальную реализацию.

Предупреждения

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

В случае, если импорт генерируется автоматически макросом, Elixir не будет выдавать предупреждений, поскольку импорт не был явно определён.

Оба поведения предупреждений можно изменить, явно установив опцию :warn в true или false.

Неоднозначные имена функций/макросов

Если два модуля A и B импортированы, и оба содержат функцию foo с арностью 1, ошибка выдаётся только при фактическом неоднозначном вызове функции foo/1; то есть ошибки выдаются лениво, а не сразу.

quote(opts, block)Source

Получение представления любого выражения.

Примеры

iex> quote do
...>   sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}

AST (Абстрактное синтаксическое дерево) Elixir

Любой код Elixir может быть представлен с помощью структур данных Elixir. Основным блоком Elixir-макросов является кортеж из трех элементов, например:

{:sum, [], [1, 2, 3]}

Кортеж выше представляет собой вызов функции sum с аргументами 1, 2 и 3. Элементы кортежа:

  • Первый элемент кортежа всегда является атомом или другим кортежем в том же представлении.

  • Второй элемент кортежа представляет метаданные.

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

Помимо описанного выше кортежа, Elixir имеет несколько литералов, которые также являются частью его AST. Эти литералы возвращают сами себя при цитировании. Это:

:sum         #=> Atoms
1            #=> Integers
2.0          #=> Floats
[1, 2]       #=> Lists
"strings"    #=> Strings
{key, value} #=> Tuples with two elements

Любое другое значение, такое как карта или четырёхэлементный кортеж, должно быть экранировано (Macro.escape/1) перед включением в AST.

Параметры

  • :bind_quoted — передаёт связывание макросу. В случае передачи связывания, unquote/1 автоматически отключается.

  • :context — устанавливает контекст разрешения.

  • :generated — помечает данный фрагмент как сгенерированный, чтобы он не выводил предупреждения. В настоящее время это работает только со специальными формами (например, вы можете аннотировать case но не if).

  • :file — устанавливает для цитируемых выражений указанный файл.

  • :line — устанавливает для цитируемых выражений указанную строку.

  • :location — при установке в :keep, сохраняет текущую строку и файл из цитирования. Подробнее см. раздел "Информация о трассировке стека" ниже.

  • :unquote — при false, отключает раскомментирование. Это означает, что любой вызов unquote будет сохранён в AST в неизменном виде, вместо замены аргументами unquote. Например:

    iex> quote do
    ...>   unquote("hello")
    ...> end
    "hello"
    iex> quote unquote: false do
    ...>   unquote("hello")
    ...> end
    {:unquote, [], ["hello"]}

Quote и макросы

quote/2 обычно используется с макросами для генерации кода. В качестве упражнения давайте определим макрос, который возводит число в квадрат. На практике нет причин для определения такого макроса (и это будет считаться плохой практикой), но он достаточно прост, чтобы сосредоточиться на важных аспектах цитирования и макросов:

defmodule Math do
  defmacro squared(x) do
    quote do
      unquote(x) * unquote(x)
    end
  end
end

Мы можем вызвать его как:

import Math
IO.puts("Got #{squared(5)}")

Поначалу в этом примере нет ничего, что явно указывало бы на то, что это макрос. Но на самом деле, во время компиляции, squared(5) превращается в 5 * 5. Аргумент 5 дублируется в сгенерированном коде. Мы можем увидеть это на практике, однако, так как наш макрос на самом деле имеет ошибку:

import Math
my_number = fn ->
  IO.puts("Returning 5")
  5
end
IO.puts("Got #{squared(my_number.())}")

Приведенный выше пример выведет:

Returning 5
Returning 5
Got 25

Обратите внимание, как "Возвращение 5" было напечатано дважды, а не один раз. Это происходит потому, что макрос получает выражение, а не значение (что мы ожидали бы от обычной функции). Это означает, что:

squared(my_number.())

Фактически расширяется до:

my_number.() * my_number.()

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

Давайте исправим наш макрос:

defmodule Math do
  defmacro squared(x) do
    quote do
      x = unquote(x)
      x * x
    end
  end
end

Теперь вызов squared(my_number.()) так же, как и раньше, выведет значение только один раз.

На самом деле, этот шаблон настолько распространен, что в большинстве случаев вы захотите использовать параметр bind_quoted с quote/2:

defmodule Math do
  defmacro squared(x) do
    quote bind_quoted: [x: x] do
      x * x
    end
  end
end

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

Прежде чем завершить это краткое введение, вы заметите, что, хотя мы определили переменную x внутри нашего цитирования:

quote do
  x = unquote(x)
  x * x
end

Когда мы вызываем:

import Math
squared(5)
x
** (CompileError) undefined variable x or undefined function x/0

Мы можем видеть, что x не попала в пользовательский контекст. Это происходит потому, что Elixir-макросы гигиеничны, тема, которую мы подробно обсудим в следующих разделах.

Гигиеничность переменных

Рассмотрим следующий пример:

defmodule Hygiene do
  defmacro no_interference do
    quote do
      a = 1
    end
  end
end

require Hygiene

a = 10
Hygiene.no_interference()
a
#=> 10

В примере выше, a возвращает 10, даже если макрос, по-видимому, устанавливает его в 1, потому что переменные, определённые в макросе, не влияют на контекст, в котором выполняется макрос. Если вы хотите установить или получить переменную в контексте вызывающего кода, вы можете сделать это с помощью макроса var!:

defmodule NoHygiene do
  defmacro interference do
    quote do
      var!(a) = 1
    end
  end
end

require NoHygiene

a = 10
NoHygiene.interference()
a
#=> 1

Вы даже не можете получить доступ к переменным, определённым в том же модуле, если не укажете явно контекст:

defmodule Hygiene do
  defmacro write do
    quote do
      a = 1
    end
  end

  defmacro read do
    quote do
      a
    end
  end
end

Hygiene.write()
Hygiene.read()
** (RuntimeError) undefined variable a or undefined function a/0

Для этого вы можете явно передать текущий область видимости модуля как аргумент:

defmodule ContextHygiene do
  defmacro write do
    quote do
      var!(a, ContextHygiene) = 1
    end
  end

  defmacro read do
    quote do
      var!(a, ContextHygiene)
    end
  end
end

ContextHygiene.write()
ContextHygiene.read()
#=> 1

Гигиеничность псевдонимов

Псевдонимы внутри цитирования по умолчанию гигиеничны. Рассмотрим следующий пример:

defmodule Hygiene do
  alias Map, as: M

  defmacro no_interference do
    quote do
      M.new()
    end
  end
end

require Hygiene
Hygiene.no_interference()
#=> %{}

Обратите внимание, что, даже если псевдоним M недоступен в контексте расширения макроса, приведенный выше код работает, потому что M всё ещё расширяется до Map.

Аналогично, даже если мы определили псевдоним с тем же именем перед вызовом макроса, это не повлияет на результат макроса:

defmodule Hygiene do
  alias Map, as: M

  defmacro no_interference do
    quote do
      M.new()
    end
  end
end

require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{}

В некоторых случаях вы хотите получить доступ к псевдониму или модулю, определённому в вызывающем коде. Для этого вы можете использовать макрос alias!:

defmodule Hygiene do
  # This will expand to Elixir.Nested.hello()
  defmacro no_interference do
    quote do
      Nested.hello()
    end
  end

  # This will expand to Nested.hello() for
  # whatever is Nested in the caller
  defmacro interference do
    quote do
      alias!(Nested).hello()
    end
  end
end

defmodule Parent do
  defmodule Nested do
    def hello, do: "world"
  end

  require Hygiene
  Hygiene.no_interference()
  ** (UndefinedFunctionError) ...

  Hygiene.interference()
  #=> "world"
end

Гигиеничность импортов

Подобно псевдонимам, импорты в Elixir гигиеничны. Рассмотрим следующий код:

defmodule Hygiene do
  defmacrop get_length do
    quote do
      length([1, 2, 3])
    end
  end

  def return_length do
    import Kernel, except: [length: 1]
    get_length
  end
end

Hygiene.return_length()
#=> 3

Обратите внимание, как Hygiene.return_length/0 возвращает 3, даже если функция Kernel.length/1 не импортирована. На самом деле, даже если return_length/0 импортировал функцию с тем же именем и арностью из другого модуля, это не повлияет на результат функции:

def return_length do
  import String, only: [length: 1]
  get_length
end

Вызов этого нового return_length/0 всё ещё вернёт 3 в качестве результата.

Elixir достаточно умён, чтобы отложить разрешение до самого последнего момента. Поэтому, если вы вызываете length([1, 2, 3]) внутри цитирования, но функция length/1 недоступна, она затем расширяется в вызывающем коде:

defmodule Lazy do
  defmacrop get_length do
    import Kernel, except: [length: 1]

    quote do
      length("hello")
    end
  end

  def return_length do
    import Kernel, except: [length: 1]
    import String, only: [length: 1]
    get_length
  end
end

Lazy.return_length()
#=> 5

Информация о трассировке стека

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

# adder.ex
defmodule Adder do
  @doc "Defines a function that adds two numbers"
  defmacro defadd do
    quote location: :keep do
      def add(a, b), do: a + b
    end
  end
end

# sample.ex
defmodule Sample do
  import Adder
  defadd
end

require Sample
Sample.add(:one, :two)
** (ArithmeticError) bad argument in arithmetic expression
    adder.ex:5: Sample.add/2

При использовании location: :keep и передаче неверных аргументов функции Sample.add/2, информация о трассировке стека будет указывать на файл и строку внутри цитирования. Без location: :keep, ошибка сообщается в том месте, где был вызван defadd . location: :keep влияет только на определения внутри цитирования.

Важно: не используйте location: :keep, если определение функции также unquote некоторые аргументы макроса. В противном случае Elixir сохранит определение файла текущего местоположения, но нецитируемые аргументы могут содержать информацию о строке вызывающего макроса, что приведёт к неверной трассировке стека.

Связывание и фрагменты раскомментирования

Механизмы Elixir quote/unquote обеспечивают функциональность фрагментов раскомментирования. Фрагменты раскомментирования предоставляют простой способ динамического генерирования функций. Рассмотрим этот пример:

kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
  def unquote(k)(), do: unquote(v)
end)

В приведённом выше примере мы динамически сгенерировали функции foo/0 и bar/0. Теперь предположим, что мы хотим преобразовать эту функциональность в макрос:

defmacro defkv(kv) do
  Enum.map(kv, fn {k, v} ->
    quote do
      def unquote(k)(), do: unquote(v)
    end
  end)
end

Мы можем вызвать этот макрос как:

defkv [foo: 1, bar: 2]

Однако, мы не можем вызвать его следующим образом:

kv = [foo: 1, bar: 2]
defkv kv

Это происходит потому, что макрос ожидает, что его аргументы будут списком ключевых слов на этапе компиляции. Так как в приведённом выше примере мы передаём представление переменной kv, наш код терпит неудачу.

Это распространённая ловушка при разработке макросов. Мы предполагаем определённую форму в макросе. Мы можем обойти эту проблему, раскомментируя переменную внутри цитируемого выражения:

defmacro defkv(kv) do
  quote do
    Enum.each(unquote(kv), fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end)
  end
end

Если вы попытаетесь запустить наш новый макрос, вы заметите, что он даже не будет компилироваться, так как переменные k и v не существуют. Это связано с неоднозначностью: unquote(k) может быть фрагментом раскомментирования, как прежде, или обычным раскомментированием, как в unquote(kv).

Одно из решений этой проблемы заключается в отключении раскомментирования в макросе, однако это сделает невозможным внедрение представления kv в дерево. Тогда параметр :bind_quoted приходит на помощь (снова!). Используя :bind_quoted, мы можем автоматически отключить раскомментирование, одновременно внедряя необходимые переменные в дерево:

defmacro defkv(kv) do
  quote bind_quoted: [kv: kv] do
    Enum.each(kv, fn {k, v} ->
      def unquote(k)(), do: unquote(v)
    end)
  end
end

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

receive(args)Source

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

В случае отсутствия такого сообщения текущий процесс зависает, ожидая прихода сообщения или истечения заданного времени ожидания.

Примеры

receive do
  {:selector, number, name} when is_integer(number) ->
    name
  name when is_atom(name) ->
    name
  _ ->
    IO.puts(:stderr, "Unexpected message received")
end

Необязательное after условие может быть указано в случае, если сообщение не было получено после заданного интервала ожидания, указанного в миллисекундах:

receive do
  {:selector, number, name} when is_integer(number) ->
    name
  name when is_atom(name) ->
    name
  _ ->
    IO.puts(:stderr, "Unexpected message received")
after
  5000 ->
    IO.puts(:stderr, "No message in 5 seconds")
end

Условие after может быть указано даже если нет условий сопоставления. Значение таймаута, задаваемое для after , может быть любым выражением, которое оценивается как одно из допустимых значений:

  • :infinity — процесс должен неограниченно ждать соответствия сообщения, это то же самое, что и отсутствие условия after

  • 0 — если в почтовом ящике нет соответствующего сообщения, таймаут произойдёт немедленно

  • положительное целое число, меньшее или равное 4_294_967_295 (0xFFFFFFFF в шестнадцатеричной нотации) — значение таймаута должно быть представлено как беззнаковое 32-битное целое число.

Обработка переменных

Специальная форма receive/1 обрабатывает переменные точно так же, как и специальный макрос case/2. Для получения дополнительной информации, см. документацию по case/2.

require(module, opts)Source

Требует модуль для использования его макросов.

Примеры

Общедоступные функции в модулях доступны глобально, но для использования макросов вам необходимо явно потребовать модуль, в котором они определены.

Предположим, вы создали собственное if/2 реализацию в модуле MyMacros. Если вы хотите вызвать её, вам необходимо сначала явно потребовать MyMacros:

defmodule Math do
  require MyMacros
  MyMacros.if do_something, it_works
end

Попытка вызвать макрос, который не был загружен, приведёт к ошибке.

Краткая запись алиаса

require/2 также принимает :as в качестве опции, чтобы автоматически создать алиас. Для получения дополнительной информации, см. alias/2.

super(args)Source

Вызывает переопределённую функцию при переопределении с помощью Kernel.defoverridable/1.

См. Kernel.defoverridable/1 для получения дополнительной информации и документации.

try(args)Source

Вычисляет заданные выражения и обрабатывает любые ошибки, выходы или броски, которые могут произойти.

Примеры

try do
  do_something_that_may_fail(some_arg)
rescue
  ArgumentError ->
    IO.puts("Invalid argument given")
catch
  value ->
    IO.puts("Caught #{inspect(value)}")
else
  value ->
    IO.puts("Success! The result was #{inspect(value)}")
after
  IO.puts("This is printed regardless if it failed or succeeded")
end

Условие rescue используется для обработки исключений, а условие catch может быть использовано для перехвата брошенных значений и выходов. Условие else может быть использовано для управления потоком на основе результата выражения. catch, rescue, и else условия работают на основе сопоставления с образцом (аналогично специальной форме case).

Вызовы внутри try/1 не являются хвостовыми рекурсивными, так как виртуальной машине необходимо сохранить стек отслеживания на случай возникновения исключения. Для получения стека отслеживания обратитесь к __STACKTRACE__/0 внутри условия rescue или catch.

Условия rescue

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

# Rescue a single exception without binding the exception
# to a variable
try do
  UndefinedModule.undefined_function
rescue
  UndefinedFunctionError -> nil
end

# Rescue any of the given exception without binding
try do
  UndefinedModule.undefined_function
rescue
  [UndefinedFunctionError, ArgumentError] -> nil
end

# Rescue and bind the exception to the variable "x"
try do
  UndefinedModule.undefined_function
rescue
  x in [UndefinedFunctionError] -> nil
end

# Rescue all kinds of exceptions and bind the rescued exception
# to the variable "x"
try do
  UndefinedModule.undefined_function
rescue
  x -> nil
end

Ошибки Erlang

Ошибки Erlang преобразуются в ошибки Elixir при перехвате:

try do
  :erlang.error(:badarg)
rescue
  ArgumentError -> :ok
end
#=> :ok

Наиболее распространённые ошибки Erlang преобразуются в их аналоги в Elixir. Те, что не преобразуются, преобразуются в более общую ошибку ErlangError:

try do
  :erlang.error(:unknown)
rescue
  ErlangError -> :ok
end
#=> :ok

Фактически, ErlangError можно использовать для перехвата любых ошибок, которые не являются надлежащими ошибками Elixir. Например, его можно использовать для перехвата предыдущей ошибки :badarg до преобразования:

try do
  :erlang.error(:badarg)
rescue
  ErlangError -> :ok
end
#=> :ok

Условия catch

Условие catch может быть использовано для перехвата брошенных значений, выходов и ошибок.

Перехват брошенных значений

catch может быть использовано для перехвата значений, брошенных с помощью Kernel.throw/1:

try do
  throw(:some_value)
catch
  thrown_value ->
    IO.puts("A value was thrown: #{inspect(thrown_value)}")
end

Перехват значений любого типа

Условие catch также поддерживает перехват выходов и ошибок. Для этого оно допускает сопоставление с типом перехваченного значения, а также с самим значением:

try do
  exit(:shutdown)
catch
  :exit, value ->
    IO.puts("Exited with value #{inspect(value)}")
end

try do
  exit(:shutdown)
catch
  kind, value when kind in [:exit, :throw] ->
    IO.puts("Caught exit or throw with value #{inspect(value)}")
end

Условие catch также поддерживает :error наряду с :exit и :throw, как в Erlang, хотя это обычно избегают в пользу механизмов управления raise/rescue. Одна из причин заключается в том, что при перехвате :error, ошибка не преобразуется автоматически в ошибку Elixir:

try do
  :erlang.error(:badarg)
catch
  :error, :badarg -> :ok
end
#=> :ok

Условия after

Условие after позволяет определять логику очистки, которая будет вызвана как при успешном выполнении блока кода, переданного try/1, так и при возникновении ошибки. Обратите внимание, что процесс выйдет как обычно при получении сигнала выхода, который заставляет его выйти внезапно, и поэтому условие after не гарантируется для выполнения. К счастью, большинство ресурсов в Elixir (такие как открытые файлы, таблицы ETS, порты, сокеты и т. д.) связаны с владеющим процессом или отслеживают его, и автоматически очистятся, если этот процесс выйдет.

File.write!("tmp/story.txt", "Hello, World")
try do
  do_something_with("tmp/story.txt")
after
  File.rm("tmp/story.txt")
end

Хотя условия after вызываются независимо от того, была ли ошибка, они не изменяют возвращаемое значение. Все следующие примеры возвращают :return_me:

try do
  :return_me
after
  IO.puts("I will be printed")
  :not_returned
end

try do
  raise "boom"
rescue
  _ -> :return_me
after
  IO.puts("I will be printed")
  :not_returned
end

Условия else

Условие else позволяет сопоставить с образцом результат тела, переданного try/1:

x = 2
try do
  1 / x
rescue
  ArithmeticError ->
    :infinity
else
  y when y < 1 and y > -1 ->
    :small
  _ ->
    :large
end

Если условие else отсутствует и исключения не возникают, возвращается результат выражения:

x = 1
^x =
  try do
    1 / x
  rescue
    ArithmeticError ->
      :infinity
  end

Однако, если условие else присутствует, но результат выражения не соответствует ни одному из шаблонов, будет выброшено исключение. Это исключение не будет перехвачено условиями catch или rescue в том же try:

x = 1
try do
  try do
    1 / x
  rescue
    # The TryClauseError cannot be rescued here:
    TryClauseError ->
      :error_a
  else
    0 ->
      :small
  end
rescue
  # The TryClauseError is rescued here:
  TryClauseError ->
    :error_b
end

Аналогично, исключение внутри условия else не перехватывается или не обрабатывается внутри того же try:

try do
  try do
    nil
  catch
    # The exit(1) call below can not be caught here:
    :exit, _ ->
      :exit_a
  else
    _ ->
      exit(1)
  end
catch
  # The exit is caught here:
  :exit, _ ->
    :exit_b
end

Это означает, что виртуальная машина больше не нуждается в сохранении стека отслеживания внутри условия else, и хвостовая рекурсия возможна при использовании try с хвостовым вызовом в качестве последнего вызова внутри условия else. То же самое относится к условиям rescue и catch.

Только результат проверенного выражения передаётся условию else Если условие try заканчивается в условиях rescue или catch, их результат не передаётся условию else:

try do
  throw(:catch_this)
catch
  :throw, :catch_this ->
    :it_was_caught
else
  # :it_was_caught will not fall down to this "else" clause.
  other ->
    {:else, other}
end

Обработка переменных

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

try do
  x = 1
  do_something_that_may_fail(same_arg)
  :ok
catch
  _, _ -> :failed
end

x
#=> unbound variable "x"

В приведённом выше примере x недоступно, так как оно было определено внутри условия try. Распространённым приёмом для решения этой проблемы является возврат переменных, определённых внутри try:

x =
  try do
    x = 1
    do_something_that_may_fail(same_arg)
    x
  catch
    _, _ -> :failed
  end

unquote(expr)Source

Расквычивает заданное выражение внутри цитируемого выражения.

Эта функция ожидает в качестве аргумента корректный AST Elixir, также известный как цитируемое выражение. Если вы хотите unquote любое значение, такое как карта или четырёхэлементная кортеж, необходимо вызвать Macro.escape/1 перед расквычиванием.

Примеры

Представьте ситуацию, когда у вас есть цитируемое выражение, и вы хотите вставить его внутрь другого цитирования. Первая попытка будет:

value =
  quote do
    13
  end

quote do
  sum(1, value, 3)
end

В которой аргумент для вызова функции :sum не является ожидаемым результатом:

{:sum, [], [1, {:value, [], Elixir}, 3]}

Для этого используем unquote:

iex> value =
...>   quote do
...>     13
...>   end
iex> quote do
...>   sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}

Если вы хотите расквычить значение, которое не является цитируемым выражением, например, карту, вам необходимо вызвать Macro.escape/1 перед этим:

iex> value = %{foo: :bar}
iex> quote do
...>   process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}

Если вы забудете это сделать, Elixir выдаст ошибку при компиляции кода.

unquote_splicing(expr)Source

Разворачивает переданный список, расширяя его аргументы.

Аналогично unquote/1.

Примеры

iex> values = [2, 3, 4]
iex> quote do
...>   sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]}

with(args)Source

Используется для объединения правил сопоставления.

Начнем с примера:

iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
{:ok, 150}

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

iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
:error

Ограничения могут использоваться и в шаблонах:

iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...>   {:ok, to_string(role)}
...> end
{:ok, "admin"}

Как и в for/1, переменные, связанные внутри with/1, не будут «вытекать». Выражения без <- также могут использоваться в правилах. Например, вы можете выполнять обычные сопоставления с помощью оператора =:

iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      double_width = width * 2,
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil

Поведение любого выражения в правиле такое же, как если бы оно было написано вне with. Например, = вызовет MatchError, вместо возвращения значения, не соответствующего ни одному правилу:

with :foo = :bar, do: :ok
** (MatchError) no match of right hand side value: :bar

Как и при любом другом вызове функции или макроса в Elixir, явные скобки также могут использоваться вокруг аргументов перед блоком do-end:

iex> opts = %{width: 10, height: 15}
iex> with(
...>   {:ok, width} <- Map.fetch(opts, :width),
...>   {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...>   {:ok, width * height}
...> end
{:ok, 150}

Выбор между скобками и их отсутствием зависит от предпочтений.

Блоки else

Модифицировать возвращаемое значение with в случае неудачного сопоставления можно с помощью опции else:

iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> else
...>   :error ->
...>     {:error, :wrong_data}
...>
...>   _other_error ->
...>     :unexpected_error
...> end
{:error, :wrong_data}

Блок else работает как правило case: он может содержать несколько правил, и будет использоваться первое совпадение. Переменные, связанные внутри with (например, width в данном примере), недоступны в блоке else.

Если используется блок else и нет совпадений, возникает исключение WithClauseError.

Внимательно!

Следует учитывать, что одним из потенциальных недостатков with является то, что все правила обработки ошибок сжаты в один блок else. Например, рассмотрим код, который проверяет, указывает ли заданный путь на файл Elixir, и существует ли он перед созданием резервной копии:

with ".ex" <- Path.extname(path),
     true <- File.exists?(path) do
  backup_path = path <> ".backup"
  File.cp!(path, backup_path)
  {:ok, backup_path}
else
  binary when is_binary(binary) ->
    {:error, :invalid_extension}

  false ->
    {:error, :missing_file}
end

Обратите внимание, как нам приходится восстанавливать типы результатов Path.extname/1 и File.exists?/1 для построения сообщений об ошибках. В этом случае лучше изменить правила with так, чтобы они сразу возвращали желаемый формат, как в этом примере:

with :ok <- validate_extension(path),
     :ok <- validate_exists(path) do
  backup_path = path <> ".backup"
  File.cp!(path, backup_path)
  {:ok, backup_path}
end

defp validate_extension(path) do
  if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end

defp validate_exists(path) do
  if File.exists?(path), do: :ok, else: {:error, :missing_file}
end

Обратите внимание, насколько упорядоченный и понятный становится код, когда каждое правило в else возвращает нормализованный формат.

^varSource

Оператор фиксации. Доступ к уже связанной переменной в правилах сопоставления.

Примеры

Elixir позволяет переприсваивать переменные через статичное однократное присваивание:

iex> x = 1
iex> x = x + 1
iex> x
2

Однако в некоторых ситуациях полезно сопоставлять существующее значение, а не переприсваивать его. Это можно сделать с помощью специальной формы ^, разговорно называемой оператором фиксации:

iex> x = 1
iex> ^x = List.first([1])
iex> ^x = List.first([2])
** (MatchError) no match of right hand side value: 2

Обратите внимание, что ^x всегда ссылается на значение x до сопоставления. Следующий пример будет совпадать:

iex> x = 0
iex> {x, ^x} = {1, 0}
iex> x
1

{args}Source

Создает кортеж.

Дополнительную информацию о типе данных кортеж и о функциях для работы с кортежами можно найти в модуле Tuple; некоторые функции для работы с кортежами также доступны в модуле Kernel (например, Kernel.elem/2 или Kernel.tuple_size/1).

Представление AST

Только кортежи из двух элементов считаются литералами в Elixir и возвращают сами себя при цитировании. Поэтому все остальные кортежи в AST представляются как вызовы специальной формы :{}.

iex> quote do
...>   {1, 2}
...> end
{1, 2}

iex> quote do
...>   {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}

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

Spec-Zone.ru

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