Spec-Zone.ru › Elixir 1.15

Kernel.SpecialForms

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

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

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

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

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

Функции

%struct{}

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

%{}

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

&expr

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

left . right

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

__aliases__(args)

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

__block__(args)

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

__CALLER__

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

__DIR__

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

__ENV__

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

__MODULE__

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

__STACKTRACE__

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

left :: right

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

<<args>>

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

left = right

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

alias(module, opts)

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

case(condition, clauses)

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

cond(clauses)

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

fn(clauses)

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

for(args)

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

import(module, opts)

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

quote(opts, block)

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

receive(args)

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

require(module, opts)

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

super(args)

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

try(args)

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

unquote(expr)

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

unquote_splicing(expr)

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

with(args)

Комбинирует сопоставляющие разделы.

^var

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

{args}

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

END_OF_DOCUMENT_MARKER ```

%struct{}Source

Соответствует структуре или создаёт её.

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

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

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

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

%User{}

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

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

Поля структуры можно указать при создании структуры:

%User{age: 31}
#=> %{__struct__: User, name: "john", age: 31}

Или также при сопоставлении с образцом для извлечения значений:

%User{age: age} = user

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

%User{user | age: 28}

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

%User{full_name: "john doe"}

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

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

Сопоставление с образцом имён структур

Помимо возможности сопоставления с образцом полей структуры, например:

%User{age: age} = user

Структуры также позволяют сопоставлять с образцом имя структуры:

%struct_name{} = user
struct_name #=> User

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

%_{} = user

%{}Source

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

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

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

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

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

&exprSource

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

Захват

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

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

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

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

&local_function/1

См. также Function.capture/3.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

left . rightSource

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

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

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

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

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

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

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

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

iex> Hello.World
Hello.World

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

Синтаксис

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

iex> Kernel.Sample
Kernel.Sample

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

iex> Kernel.+(1, 2)
3

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

Оборачивание имени функции в одинарные или двойные кавычки всегда является удалённым вызовом. Следовательно, Kernel."Foo" попытается вызвать функцию "Foo", а не вернуть псевдоним Kernel.Foo. Это сделано по дизайну, так как имена модулей более жёсткие, чем имена функций.

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

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

Процитированное выражение

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

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

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

{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}

Этот кортеж следует общему структуре процитированных выражений в Elixir: имя как первый аргумент, некоторый список ключевых слов как метаданные как второй, и список аргументов как третий. В этом случае аргументами являются псевдоним String и атом :downcase. Второй аргумент в удалённом вызове всегда является атомом.

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

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

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

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

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

Дескрипция

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

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

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

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

__aliases__(args)Source

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

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

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

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

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

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

__CALLER__Source

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

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

__DIR__Source

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

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

__ENV__Source

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

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

__MODULE__Source

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

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

__STACKTRACE__Source

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

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

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

left :: rightSource

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

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

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

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

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

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

<<args>>Source

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

Примеры

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

Типы

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

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

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

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

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

iex> <<0, "foo">>
<<0, 102, 111, 111>>

Двоичные данные необходимо явно пометить как binary:

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

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

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

В противном случае при построении двоичных данных будет получено исключение ArgumentError:

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

Параметры

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

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

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

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

Размер может быть переменной или любым допустимым выражением условия:

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

Размер может использовать предыдущие переменные, определённые в самой битовой строке/двоичных данных:

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

Однако он не может использовать переменные, определенные в совпадении за пределами битовой строки/двоичных данных:

{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment

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

<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">>
** (CompileError): a binary field without size is only allowed at the end of a binary pattern

Сокращенная запись

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

iex> x = 1
iex> <<x::8>> == <<x::size(8)>>
true
iex> <<x::8*4>> == <<x::size(8)-unit(4)>>
true

Этот синтаксис отражает тот факт, что эффективный размер определяется умножением размера на единицу.

Модификаторы

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

Модификатор Применимые типы
signed integer
unsigned (по умолчанию) integer
little integer, float, utf16, utf32
big (по умолчанию) integer, float, utf16, utf32
native integer, utf16, utf32

Знак

Целые числа могут быть signed или unsigned, по умолчанию unsigned.

iex> <<int::integer>> = <<-100>>
<<156>>
iex> int
156
iex> <<int::integer-signed>> = <<-100>>
<<156>>
iex> int
-100

signed и unsigned используются только для сопоставления двоичных данных (см. ниже) и только для целых чисел.

iex> <<-100::signed, _rest::binary>> = <<-100, "foo">>
<<156, 102, 111, 111>>

Порядок байтов

В Elixir есть три варианта порядка байтов: big, little, и native. По умолчанию big:

iex> <<number::little-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
256
iex> <<number::big-integer-size(16)>> = <<0, 1>>
<<0, 1>>
iex> number
1

native определяется виртуальной машиной при запуске и зависит от операционной системы хоста.

Сопоставление двоичных/битовых строк

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

Сопоставление двоичных данных может использоваться самостоятельно для извлечения информации из двоичных данных:

iex> <<"Hello, ", place::binary>> = "Hello, World"
"Hello, World"
iex> place
"World"

Или как часть определения функций для сопоставления шаблонов:

defmodule ImageTyper do
  @png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
                   13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
  @jpg_signature <<255::size(8), 216::size(8)>>

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

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

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

ERL_COMPILER_OPTIONS=bin_opt_info mix compile

Дополнительную информацию о конкретных оптимизациях и вопросах производительности см. в "Constructing and matching binaries" chapter of the Erlang's Efficiency Guide.

left = rightSource

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

END_OF_DOCUMENT_MARKER

alias(module, opts)Source

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

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

Примеры

case File.read(file) do
  {:ok, contents} when is_binary(contents) ->
    String.split(contents, "\n")

  {:error, _reason} ->
    Logger.warning "could not find #{file}, assuming empty..."
    []
end

В примере выше, мы сопоставляем результат File.read/1 с каждой частью "заголовка" и выполняем соответствующую часть "тела" первой совпавшей части.

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

x = 10

case x do
  0 ->
    "This clause won't match"

  _ ->
    "This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"

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

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

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

value
#=> unbound variable value

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

value = 7

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

value
#=> 7

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

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

x = 1

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

Использование охранников для сопоставления с несколькими значениями

Хотя невозможно сопоставить несколько шаблонов в одной части, можно сопоставить несколько значений с помощью охранников:

case data do
  value when value in [:one, :two] ->
    "#{value} has been matched"

  :three ->
    "three has been matched"
end

cond(clauses)Source

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

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

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

См. Function для получения дополнительной информации.

Примеры

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

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

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

iex> for n <- [1, 2, 3, 4], do: n * 2
[2, 4, 6, 8]

Понимание принимает множество генераторов и фильтров. for использует оператор <- для извлечения значений из перечислимого объекта справа и сопоставления их с шаблоном слева. Мы называем их генераторами:

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

Фильтры должны оцениваться как истинностные значения (всё, кроме nil и false). Если фильтр ложный, то текущее значение отбрасывается.

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

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

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

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

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent = languages[parent], do: {language, grandparent}
[elixir: :prolog]

Учитывая, что прародителями Erlang и Prolog были nil, эти значения были отфильтрованы. Если вы не хотите этого поведения, простым вариантом является перемещение фильтра внутрь блока do:

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages do
...>   grandparent = languages[parent]
...>   {language, grandparent}
...> end
[elixir: :prolog, erlang: nil, prolog: nil]

Однако такой вариант не всегда доступен, так как могут быть дополнительные фильтры. Альтернативой является преобразование фильтра в генератор, заключив правую часть = в список:

iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent <- [languages[parent]], do: {language, grandparent}
[elixir: :prolog, erlang: nil, prolog: nil]

Опции :into и :uniq

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

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

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

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

for line <- IO.stream(), into: IO.stream() do
  String.upcase(line)
end

Аналогично, опция uniq: true также может быть задана для пониманий, чтобы гарантировать, что результаты добавляются в коллекцию только в том случае, если они ещё не были возвращены. Например:

iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2
[2, 4, 6]

iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>>
"ABC"

Опция :reduce

Хотя опция :into позволяет нам настроить поведение понимания для определённого типа данных, например, поместить все значения в карту или в двоичную строку, этого не всегда достаточно.

Например, представьте, что у вас есть двоичная строка с буквами, где вы хотите посчитать, сколько раз встречается каждая строчная буква, игнорируя все прописные. Например, для строки "AbCabCABc", мы хотим вернуть карту %{"a" => 1, "b" => 2, "c" => 1}.

Если бы мы использовали :into, нам понадобился бы тип данных, который вычисляет частоту каждого элемента, который он содержит. Хотя такого типа данных в Elixir нет, вы можете реализовать его самостоятельно.

Более простым вариантом было бы использовать понимания для сопоставления и фильтрации букв, а затем вызвать Enum.reduce/3 для построения карты, например:

iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}

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

К счастью, понимания также поддерживают опцию :reduce , которая позволит нам объединить оба вышеуказанных шага в один шаг:

iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...>   acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}

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

END_OF_DOCUMENT_MARKER

import(module, opts)Source

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

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
import Kernel, only: :sigils

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

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

Примеры

iex> quote do
...>   sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}

AST Elixir (Абстрактное синтаксическое дерево)

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

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

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

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

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

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

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

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

Любое другое значение, например, карта или кортеж из четырёх элементов, должно быть экранировано (Macro.escape/1) перед вводом в AST.

Параметры

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

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

  • :generated — помечает данный фрагмент как сгенерированный, чтобы не выводить предупреждения. Это также полезно для предотвращения сообщений об ошибках dialyzer при генерации макросами неиспользуемых клаузул.

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

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

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

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

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

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

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

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

Его можно вызвать так:

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

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

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

В приведённом примере будет выведено:

Returning 5
Returning 5
Got 25

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

squared(my_number.())

В действительности расширяется до:

my_number.() * my_number.()

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

Исправим наш макрос:

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

Теперь вызов squared(my_number.()) как и раньше выведет значение только один раз.

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

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

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

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

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

При вызове:

import Math
squared(5)
x
** (CompileError) undefined variable "x"

Мы видим, что 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

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

quote(do: var)
#=> {:var, [], Elixir}

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

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

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

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

location: :keep и разыменование

Не используйте location: :keep, если определение функции также unquote некоторые аргументы макроса. В противном случае Elixir сохранит определение файла текущего расположения, но аргументы разыменования могут содержать информацию о строке из вызывающего макрос, что приведёт к ошибочным отслеживаниям стека.

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

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

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

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

Примеры

receive do
  {:selector, number, name} when is_integer(number) ->
    name
  name when is_atom(name) ->
    name
  _ ->
    IO.puts(:stderr, "Unexpected message received")
end

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

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

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

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

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

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

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

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

require(module, opts)Source

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

Примеры

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

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

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

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

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

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

super(args)Source

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

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

try(args)Source

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

Примеры

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

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

Вызовы внутри try/1 не являются хвостовыми рекурсиями, так как ВМ необходимо сохранить стек вызовов на случай возникновения исключения. Чтобы получить стек вызовов, обратитесь к __STACKTRACE__/0 внутри условия rescue или catch.

rescue условия

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

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

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

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

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

Ошибки Erlang

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

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

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

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

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

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

catch условия

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

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

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

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

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

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

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

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

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

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

after условия

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

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

Хотя after условия вызываются независимо от того, произошла ли ошибка, они не изменяют возвращаемое значение. Все следующие примеры возвращают :return_me:

try do
  :return_me
after
  IO.puts("I will be printed")
  :not_returned
end

try do
  raise "boom"
rescue
  _ -> :return_me
after
  IO.puts("I will be printed")
  :not_returned
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
END_OF_DOCUMENT_MARKER

unquote(expr)Source

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

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

Примеры

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

value =
  quote do
    13
  end

quote do
  sum(1, value, 3)
end

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

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

Для этого мы используем unquote:

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

Если вы хотите раскрыть значение, которое не является цитируемым выражением, например, карту, вам нужно вызвать Macro.escape/1 предварительно:

iex> value = %{foo: :bar}
iex> quote do
...>   process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}

Если вы забудете это сделать, Elixir выдаст ошибку при компиляции кода.

unquote_splicing(expr)Source

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

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

Комбинирует сопоставляющие правила.

Один из способов понять with – показать, какие шаблоны кода он улучшает. Представьте себе карту, где поля width и height являются необязательными, и вы хотите вычислить её площадь, как {:ok, area} или вернуть :error. Мы могли бы реализовать эту функцию как:

def area(map) do
  case Map.fetch(opts, :width) do
    {:ok, width} ->
      case Map.fetch(opts, :height) do
        {:ok, height} -> {:ok, width * height}
        :error -> :error
      end

    :error ->
      :error
  end
end

При вызове как area(%{width: 10, height: 15}), она должна вернуть {:ok, 150}. Если какие-либо поля отсутствуют, она возвращает :error.

Хотя приведенный выше код работает, он довольно громоздкий. Используя with, мы можем переписать его как:

def area(map) do
  with {:ok, width} <- Map.fetch(opts, :width),
       {:ok, height} <- Map.fetch(opts, :height) do
    {:ok, width * height}
  end
end

Вместо определения вложенных case с правилами, мы используем with вместе с оператором PATTERN <- EXPRESSION для сопоставления выражений справа от него с шаблоном слева. Рассматривайте <- как брата =, за исключением того, что, в то время как = генерирует исключение в случае несовпадения, <- просто прервёт цепочку with и вернёт несопоставленное значение.

Давайте попробуем это в IEx:

iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
{:ok, 150}

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

iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, width * height}
...> end
:error

В шаблонах также можно использовать условия:

iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...>   {:ok, to_string(role)}
...> end
{:ok, "admin"}

Как и в for/1, переменные, привязанные внутри with/1, недоступны за пределами with/1.

В правилах также могут использоваться выражения без <-. Например, вы можете выполнить обычные сопоставления с помощью оператора =:

iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...>      double_width = width * 2,
...>      {:ok, height} <- Map.fetch(opts, :height) do
...>   {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil

Поведение любого выражения в правиле такое же, как если бы оно было написано за пределами with. Например, = вызовет исключение MatchError, а не вернёт несопоставленное значение:

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

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

iex> opts = %{width: 10, height: 15}
iex> with(
...>   {:ok, width} <- Map.fetch(opts, :width),
...>   {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...>   {:ok, width * height}
...> end
{:ok, 150}

Выбор между скобками и их отсутствием зависит от предпочтений.

Правила else

Можно указать опцию 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}
...>
...>   _other_error ->
...>     :unexpected_error
...> end
{:error, :wrong_data}

Блок else работает как правило case: он может иметь несколько правил, и будет использоваться первое совпадение. Переменные, привязанные внутри with (например, width в этом примере), недоступны в блоке else.

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

Внимание!

Следует помнить, что одним из потенциальных недостатков with является то, что все правила обработки ошибок объединяются в один блок else. Например, рассмотрим этот код, который проверяет, указывает ли данный путь на файл Elixir и существует ли он, прежде чем создать резервную копию:

with ".ex" <- Path.extname(path),
     true <- File.exists?(path) do
  backup_path = path <> ".backup"
  File.cp!(path, backup_path)
  {:ok, backup_path}
else
  binary when is_binary(binary) ->
    {:error, :invalid_extension}

  false ->
    {:error, :missing_file}
end

Обратите внимание, как нам приходится восстанавливать типы результатов Path.extname/1 и File.exists?/1 для создания сообщений об ошибках. В этом случае лучше переписать код, чтобы каждый <- уже возвращал желаемый формат в случае ошибок, как это:

with :ok <- validate_extension(path),
     :ok <- validate_exists(path) do
  backup_path = path <> ".backup"
  File.cp!(path, backup_path)
  {:ok, backup_path}
end

defp validate_extension(path) do
  if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end

defp validate_exists(path) do
  if File.exists?(path), do: :ok, else: {:error, :missing_file}
end

Обратите внимание, как приведенный выше код лучше организован и понятнее, если мы убедимся, что каждый <- в with возвращает нормализованный формат.

^varSource

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

Примеры

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

{args}Source

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

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

Spec-Zone.ru

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