Spec-Zone.ru › Elixir 1.6

Kernel.SpecialForms

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

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

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

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

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

Функции

%

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

%{}

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

&(expr)

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

left . right

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

left :: right

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

<<args>>

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

left = right

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

^var

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

__CALLER__

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

__DIR__

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

__ENV__

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

__MODULE__

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

__aliases__(args)

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

__block__(args)

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

alias(module, opts)

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

case(condition, clauses)

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

cond(clauses)

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

fn [clauses] end

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

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)

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

with(args)

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

{args}

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

Функции

% (макрос)

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

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

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

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

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

%User{}

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

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

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

%User{full_name: "john doe"}

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

%User{user | age: 28}

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

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

%{} (макрос)

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

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

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

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

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

&(expr) (макрос)

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

Захват

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

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

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

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

&local_function/1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

left . right (макрос)

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

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

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

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

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

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

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

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

iex> Hello.World
Hello.World

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

Синтаксис

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

iex> Kernel.Sample
Kernel.Sample

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

iex> Kernel.+(1, 2)
3

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

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

Обратите внимание, что Kernel."FUNCTION_NAME" будет обрабатываться как удаленный вызов, а не как псевдоним. Такое решение принято для того, чтобы каждый раз, когда используются одинарные или двойные кавычки, происходил удаленный вызов независимо от содержимого кавычек. Это решение также отражено в обсуждаемых ниже выражениях в кавычках.

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

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
...>   String."downcase"("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}

Кортеж, содержащий :., заключён в другой кортеж, который фактически представляет собой вызов функции и имеет "FOO" в качестве аргумента.

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

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

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

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

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

Разъяснение

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

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

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

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

left :: right (макрос)

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

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

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

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

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

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

<<args>> (макрос)

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

Примеры

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

Типы

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

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

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

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

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

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 предназначены для кодовых точек Юникода. Они также могут применяться к литеральным строкам и charlist:

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, соответствующим бинарным 32 и 64 IEEE 754 соответственно.

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

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

Отсутствие указания размера для непоследней строки приведёт к ошибке компиляции:

<<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
  @png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
                   13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
  @jpg_signature <<255::size(8), 216::size(8)>>

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

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

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

ERL_COMPILER_OPTIONS=bin_opt_info mix compile

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

left = right (макрос)

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

^var (макрос)

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

Примеры

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

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

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

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

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

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

__CALLER__ (макрос)

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

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

__DIR__ (макрос)

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

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

__ENV__ (макрос)

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

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

__MODULE__ (макрос)

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

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

__aliases__(args) (макрос)

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

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

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

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

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

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

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

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

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

  3. Когда первый элемент псевдонимов является атомом :Elixir, расширение не происходит.

__block__(args) (макрос)

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

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

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

alias(module, opts) (макрос)

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

Примеры

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

defmodule Math do
  alias MyKeyword, as: Keyword
end

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

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

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

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

alias Foo.Bar.Baz

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

alias Foo.Bar.Baz, as: Baz

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

alias Foo.{Bar, Baz, Biz}

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

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

Область видимости

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

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

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

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

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

case(condition, clauses) (макрос)

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

Примеры

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

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

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

x = 10

case x do
  0 ->
    "This clause won't match"
  _ ->
    "This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"

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

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

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

value #=> unbound variable value

Однако, переменные, явно связанные в теле клаузы, доступны из внешнего контекста:

value = 7

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

value #=> 7 or 13

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

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

x = 1

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

cond(clauses) (макрос)

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

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

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

Примеры

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

fn [clauses] end (макрос)

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

Примеры

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

В примерах выше возвращаемым результатом понимания всегда был список. Возвращаемый результат можно настроить, передав опцию :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

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"

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

Объяснение

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

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

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

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

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

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

Опции

  • :unquote — когда false, отключает раскомментирование. Полезно, когда у вас есть цитата внутри другой цитаты и вы хотите контролировать, какая цитата может быть раскомментирована.

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

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

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

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

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

Литералы цитаты

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

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

Цитаты и макросы

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

Теперь вызов square(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 может быть использован во многих случаях и считается хорошей практикой не только потому, что помогает избежать распространённых ошибок, но и потому, что позволяет использовать другие инструменты, предоставляемые макросами, такие как фрагменты unquote, обсуждаемые в некоторых разделах ниже.

Прежде чем мы закончим это краткое введение, вы заметите, что, хотя мы определили переменную 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 влияет только на определения внутри цитаты.

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

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

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

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

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

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

defkv [foo: 1, bar: 2]

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

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

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

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

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

Если вы попытаетесь запустить новый макрос, он не будет компилироваться, жалуясь, что переменные k и v не существуют. Это связано с неоднозначностью: unquote(k) может быть фрагментом unquote, как прежде, или обычным unquote, как в 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, i, value} when is_integer(i) ->
    value
  value when is_atom(value) ->
    value
  _ ->
    IO.puts :stderr, "Unexpected message received"
end

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

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

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

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

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

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

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

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

require(module, opts) (macro)

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

Примеры

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

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

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

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

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

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

super(args) (macro)

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

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

try(args) (macro)

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

Примеры

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

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

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

rescue записи

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

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

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

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

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

Ошибки Erlang

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

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

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

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

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

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

catch записи

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

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

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

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

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

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

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

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

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

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

after записи

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

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

else записи

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

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

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

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

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

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

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

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

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

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

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

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

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

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

x #=> unbound variable "x"

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

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

unquote(expr) (macro)

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

Примеры

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

value = 13
quote do
  sum(1, value, 3)
end

Что вернёт:

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

Это не ожидаемый результат. Для этого используется unquote:

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

unquote_splicing(expr) (macro)

Раскрывает заданный список, расширяя его аргументы. Аналогично 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) (macro)

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

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

iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height),
...>      do: {:ok, width * height}
{: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}
: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)}
{: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}
{:ok, 300}
iex> width
nil

Обратите внимание, что если «голое выражение» не соответствует, это вызовет MatchError вместо возврата несопоставленного значения:

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

Вариант 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} (macro)

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

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

Spec-Zone.ru

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