Spec-Zone.ru › Elixir 1.10

Kernel.SpecialForms

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

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

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

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

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

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

Функции

%struct{}

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

%{}

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

&(expr)

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

left . right

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

left :: right

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

<<args>>

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

left = right

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

^var

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

__CALLER__

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

__DIR__

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

__ENV__

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

__MODULE__

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

__STACKTRACE__

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

__aliases__(args)

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

__block__(args)

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

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

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

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.

Разъяснение

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

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 двоичным 32 и 64 соответственно.

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

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 utf32, utf16, float, integer
(по умолчанию)big utf32, utf16, float, integer
native utf32, utf16, integer

Знак

Целые числа могут быть 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]}

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

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

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

Если вы хотите сопоставить шаблон с существующей переменной, используйте оператор ^/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]

После двух вызовов import выше, будет импортирована только 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.

Параметры

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

  • :context — задаёт контекст разрешения.

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

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

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

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

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

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

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

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

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

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

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

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

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

Пример выше выведет:

Returning 5
Returning 5
Got 25

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

squared(my_number.())

На самом деле расширяется до:

my_number.() * my_number.()

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

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

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

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

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

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

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

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

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

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

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

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

Гигиена в переменных

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

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

require Hygiene

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

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

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

require NoHygiene

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

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

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

  defmacro read do
    quote do
      a
    end
  end
end

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

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

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

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

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

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

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

defmodule Hygiene do
  alias Map, as: M

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

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

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

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

defmodule Hygiene do
  alias Map, as: M

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

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

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

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

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

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

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

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

Гигиена в импортах

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

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

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

Hygiene.return_length()
#=> 3

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

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

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

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

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

    quote do
      length("hello")
    end
  end

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

Lazy.return_length()
#=> 5

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

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

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

# sample.ex
defmodule Sample do
  import Adder
  defadd
end

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

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

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

Механизмы цитирования/цитирования 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)

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

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

Примеры

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:

# 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 . Если выражение оказывается в условиях 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.

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

Spec-Zone.ru

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