Spec-Zone.ru › Elixir 1.8

Kernel.SpecialForms

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

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

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

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

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

Функции

%struct{}

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

%{}

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

&(expr)

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

left . right

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

left :: right

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

<<args>>

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

left = right

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

^var

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

__CALLER__

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

__DIR__

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

__ENV__

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

__MODULE__

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

__STACKTRACE__

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

__aliases__(args)

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

__block__(args)

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

alias(module, opts)

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

case(condition, clauses)

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

cond(clauses)

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

fn

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

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)

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

{args}

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

Функции

%struct{}

(макрос)

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

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

Структуры обычно определяются с помощью макроса 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

%{}

(макрос)

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

Дополнительную информацию о картах, их синтаксисе и способах доступа и обработки см. в модуле Map.

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

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

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

&(expr)

(макрос)

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

Захват

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

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

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

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

&local_function/1

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

Оператор захвата также может использоваться для частичного применения функций, где &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 . right

(макрос)

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

Точка (.) в 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.

Развенчивание

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

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]}

left :: right

(макрос)

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

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

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

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

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

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

<<args>>

(макрос)

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

Примеры

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

Переменные или любой другой тип необходимо явно пометить:

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

Мы можем решить эту проблему, явно пометив её как binary:

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

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

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

Параметры

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

<<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 должно быть 32 или 64, что соответствует IEEE 754 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

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

left = right

(макрос)

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

^var

(макрос)

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

Примеры

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

__CALLER__

(макрос)

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

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

__DIR__

(макрос)

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

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

__ENV__

(макрос)

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

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

__MODULE__

(макрос)

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

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

__STACKTRACE__

(макрос)

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

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

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

__aliases__(args)

(макрос)

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

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

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)

(макрос)

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

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

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

alias(module, opts)

(макрос)

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)

(макрос)

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

Примеры

case thing do
  {:selector, i, value} when is_integer(i) ->
    value
  value ->
    value
end

В примере выше мы сопоставляем thing с каждым «заголовком» условия и выполняем соответствующее «тело» условия, первое совпадение.

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

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 or 13

В примере выше, значение будет 7 или 13 в зависимости от значения lucky?. В случае, если у value нет предыдущего значения перед выражением case, условия, которые не явно привязывают значение, привязывают переменную к nil.

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

x = 1

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

cond(clauses)

(макрос)

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

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

(макрос)

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

Примеры

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)

(макрос)

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

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

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]

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

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}]

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

Опции :into и :uniq

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

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

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

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

for line <- IO.stream(:stdio, :line), into: IO.stream(:stdio, :line) 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)

(макрос)

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

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

В качестве альтернативы 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)

(макрос)

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

Примеры

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.

Опции

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

    iex> quote do
    ...>   unquote("hello")
    ...> end
    "hello"
    
    iex> quote unquote: false do
    ...>   unquote("hello")
    ...> end
    {:unquote, [], ["hello"]}
  • :location - при значении :keep, сохраняет текущую строку и файл из цитирования. Подробности о информации об отслеживании стека см. ниже.

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

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

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

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

Цитирование и макросы

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

Заметьте, как «Returning 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 влияет только на определения внутри цитирования.

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

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

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)

(macro)

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

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

Примеры

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 - процесс должен неопределённо ждать соответствующего сообщения, это то же самое, что и не использовать условие после

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

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

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

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

require(module, opts)

(macro)

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

Примеры

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

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

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

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

Сокращение псевдонима

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

super(args)

(macro)

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

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

try(args)

(macro)

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

Примеры

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 succeed"
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

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)

(macro)

Отключает кавычки из заданного выражения внутри выражения в кавычках.

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

Примеры

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

value =
  quote do
    13
  end

quote do
  sum(1, value, 3)
end

Что затем вернёт:

{: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)

(макрос)

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

Аналогично 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)

(макрос)

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

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

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

Поведение любого выражения в правиле такое же, как и вне его. Например, = сгенерирует 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 в случае неудачи сопоставления:

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}
...> end
{:error, :wrong_data}

Если используется блок else и нет сопоставляемых правил, генерируется исключение WithClauseError.

{args}

(макрос)

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

Дополнительную информацию о типе данных кортеж и о функциях для работы с кортежами можно найти в модуле 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.8.2/Kernel.SpecialForms.html

Spec-Zone.ru

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