Spec-Zone.ru › Elixir 1.9

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}

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

END_OF_DOCUMENT_MARKER

Функции

%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

См. также 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 . 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.

Развязывание

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

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 для получения дополнительной информации о типах и битовых строках соответственно.

END_OF_DOCUMENT_MARKER

<<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 по эффективной обработке двоичных данных.

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

В примере выше, value будет 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. Эти литералы возвращают сами себя при использовании quote. Это:

: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, сохраняет текущую строку и файл из quote. Подробную информацию см. в разделе Информация об отслеживании стека ниже.

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

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

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

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

Quote и макросы

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

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:

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

Гигиена псевдонимов

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

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]) внутри quote, но функция 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

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

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

# 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 получает недопустимые аргументы, информация об отслеживании стека будет указывать на файл и строку внутри quote. Без location: :keep, ошибка сообщается в том месте, где был вызван defadd. location: :keep влияет только на определения внутри quote.

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

Механизмы quote/unquote 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 рекомендуется каждый раз, когда необходимо ввести значение в quote.

receive(args)

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

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

Примеры

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)

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

Примеры

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

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

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

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

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

require/2 также принимает :as в качестве параметра, чтобы автоматически создать псевдоним. Подробнее см. в alias/2.

super(args)

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

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

try(args)

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

Примеры

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

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

Эта функция ожидает допустимое выражение 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.9.4/Kernel.SpecialForms.html

Spec-Zone.ru

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