Spec-Zone.ru › Elixir 1.3

Kernel.SpecialForms

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

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

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

Наконец, он также документирует две специальные формы, __block__ и __aliases__, которые не предназначены для прямого вызова разработчиком, но они появляются в цитируемом содержании, поскольку они необходимы в конструкциях 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}

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

%User{full_name: "john doe"}

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

%User{user | age: 28}

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

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

%{} (макрос)

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

Карты — это хранилища ключ-значение, где ключи сравниваются с помощью оператора сопоставления (===). Карты могут быть созданы с помощью специальной формы %{}, где ключи связаны через =>:

%{1 => 2}

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

%{hello: :world, with: :keywords}
%{:hello => :world, with: :keywords}

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

iex> %{a: :b, a: :c}
%{a: :c}

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

Синтаксис доступа

Помимо функций доступа, доступных в модуле Map, таких как Map.get/3 и Map.fetch/2, к карте можно получить доступ, используя оператор .:

iex> map = %{a: :b}
iex> map.a
:b

Обратите внимание, что оператор . ожидает, что ключ :a существует в карте. Если нет, генерируется ошибка ArgumentError.

Синтаксис обновления

Карты также поддерживают синтаксис обновления:

iex> map = %{:a => :b}
iex> %{map | :a => :c}
%{:a => :c}

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

Представление 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> fun = &is_atom(&1)
iex> fun.(:atom)
true

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

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)
[1 | 2]

Единственное ограничение при создании анонимных функций заключается в том, что должен присутствовать как минимум один заполнитель, то есть он должен содержать как минимум &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> 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."HELLO" будет обрабатываться как удалённый вызов, а не как псевдоним. Такое решение было принято, чтобы каждый раз, когда используются одинарные или двойные кавычки, у нас был удалённый вызов, независимо от содержимого кавычек. Это решение также отражено в обсуждаемых ниже выражениях в кавычках.

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

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

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
...>   Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}

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

Раскуорирование

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

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

Подобно Kernel."HELLO", 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
  • binary
  • bytes (псевдоним для бинарного)
  • 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

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

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

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

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

Параметры

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

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

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

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

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

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

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

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

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

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

<<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, utf16, utf32
big (по умолчанию) integer, 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 по производительности обработки бинарных данных.

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__, этот макрос является удобным сокращением.

__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

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

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

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, которая принимает любую структуру, если она реализует протокол 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

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

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

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

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

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

Вызов этого нового return_length по-прежнему вернёт 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, 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 — если в почтовом ящике нет подходящего сообщения, тайм-аут произойдёт немедленно

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

Специальная форма 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) (macro)

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

Примеры

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

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

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

Блоки rescue

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

try do
  UndefinedModule.undefined_function
rescue
  UndefinedFunctionError -> nil
end

try do
  UndefinedModule.undefined_function
rescue
  [UndefinedFunctionError] -> nil
end

# rescue and bind to x
try do
  UndefinedModule.undefined_function
rescue
  x in [UndefinedFunctionError] -> nil
end

# rescue all and bind to x
try do
  UndefinedModule.undefined_function
rescue
  x -> nil
end

Ошибки Erlang

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

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

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

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

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

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

Перехват бросков и завершений

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

try do
  exit(:shutdown)
catch
  :exit, :shutdown -> IO.puts "Exited with shutdown reason"
end

try do
  throw(:sample)
catch
  :throw, :sample ->
    IO.puts "sample thrown"
end

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

Блоки after

Блок after позволяет определить логику очистки, которая будет вызвана как в случае успешного выполнения блока try, так и при возникновении ошибки. Обратите внимание, что процесс завершит работу, как обычно, при получении сигнала завершения, из-за которого он неожиданно завершит работу, и поэтому блок 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 позволяют сопоставлять результат выражения с образцом:

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 can not 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, и поэтому возможна хвостовая рекурсия при использовании блока else с хвостовым вызовом как финальным вызовом внутри блока else. То же самое верно для блоков try и rescue и catch.

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

Поскольку выражение внутри блока 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

Параметр 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)

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

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

Удобства для работы с кортежами можно найти в модуле Tuple. Некоторые функции для работы с кортежами также доступны в Kernel, а именно Kernel.elem/2, Kernel.put_elem/3 и Kernel.tuple_size/1.

Примеры

iex> {1, 2, 3}
{1, 2, 3}

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.3.4/Kernel.SpecialForms.html

Spec-Zone.ru

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