Spec-Zone.ru › Elixir 1.13

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.

Разворот

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

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

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

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

__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.

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

END_OF_DOCUMENT_MARKER

__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

Прочитайте документацию на странице Typespec и <<>>/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 предназначены для кодовых точек 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>>

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

Чтобы узнать больше о конкретных оптимизациях и соображениях по производительности, ознакомьтесь с главой "Создание и сопоставление двоичных данных" в Руководстве по эффективности Erlang.

left = rightSource

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

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]

Понимание принимает несколько генераторов и фильтров. Перечислимые генераторы определяются с помощью <-:

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

# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x * y
[2, 3, 4, 6]

Также можно задать фильтры:

# A comprehension with a generator and a filter
iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n
[2, 4, 6]

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

iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...>   String.upcase(name)
...> end
["JOHN", "MEG"]

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

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

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

Параметры :into и :uniq

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

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

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

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

for line <- IO.stream(), 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 должен быть изменён на использование -> условий, где левая часть -> получает накопленное значение предыдущей итерации, а выражение справа должно возвращать новое значение аккумулятора. После обработки всех элементов, возвращается итоговое накопленное значение. Если элементов нет, возвращается начальное значение аккумулятора.

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

В качестве альтернативы 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 — помечает заданный фрагмент как сгенерированный, чтобы не генерировать предупреждения. В настоящее время он работает только со специальными формами (например, вы можете аннотировать case , но не if).

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

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

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

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

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

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

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

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

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

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

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

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

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

Returning 5
Returning 5
Got 25

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

squared(my_number.())

Фактически расширяется до:

my_number.() * my_number.()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

require Hygiene

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

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

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

require NoHygiene

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

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

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

  defmacro read do
    quote do
      a
    end
  end
end

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

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

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

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

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

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

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

defmodule Hygiene do
  alias Map, as: M

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

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

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

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

defmodule Hygiene do
  alias Map, as: M

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

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

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

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

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

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

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

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

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

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

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

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

Hygiene.return_length()
#=> 3

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

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

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

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

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

    quote do
      length("hello")
    end
  end

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

Lazy.return_length()
#=> 5

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

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

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

# sample.ex
defmodule Sample do
  import Adder
  defadd
end

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

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

Важно: не используйте 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

Строки else

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

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

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

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

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

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

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

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

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

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

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

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

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

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

x
#=> unbound variable "x"

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

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

unquote(expr)Source

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

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

Примеры

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

value =
  quote do
    13
  end

quote do
  sum(1, value, 3)
end

Что затем вернёт:

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

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

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

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

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

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

END_OF_DOCUMENT_MARKER

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

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

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

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

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

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

Ограничители могут использоваться и в шаблонах:

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

Как и в for/1, переменные, объявленные внутри with/1, не будут "вытекать". Выражения без <- также могут использоваться в правилах. Например, вы можете выполнять обычное сопоставление с оператором =:

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

Поведение любого выражения в правиле такое же, как если бы оно было записано вне 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 так, чтобы они возвращали желаемый формат, например так:

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_extname(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.13.4/Kernel.SpecialForms.html

Spec-Zone.ru

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