Spec-Zone.ru › Elixir 1.17

Исходный код 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.

__cursor__(args)

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

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

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

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

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

__cursor__(args)Source

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

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

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

Вы можете использовать один из utf8 (по умолчанию), utf16, и utf32 для управления кодировкой строки:

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

Что эквивалентно записи:

iex> <<?f::utf16, ?o::utf16, ?o::utf16>>
<<0, 102, 0, 111, 0, 111>>

При выполнении бинарные данные должны быть явно помечены как binary:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Модификатор Применимый тип(ы)
signed integer
unsigned (по умолчанию) integer
little integer, float, utf16, utf32
big (по умолчанию) integer, float, utf16, utf32
native integer, float, 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 ImageType 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

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

END_OF_DOCUMENT_MARKER

alias(модуль, параметры)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(условие, пункты)Source

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

case/2 опирается на сопоставление с образцом и условия для выбора пункта для выполнения. Если ваша логика не может быть выражена в рамках образцов и условий, рассмотрите использование if/2 или cond/1 вместо этого.

Примеры

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 внутри выражений case, рассмотрите использование with/1.

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

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

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(пункты)Source

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

Примеры

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

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

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

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

Если ваш cond имеет два пункта, и последний переходит к true, вы можете рассмотреть использование if/2 вместо этого.

fn(пункты)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

Доступен начиная с Elixir v1.8.

В то время как параметр :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
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; то есть, ошибки выводятся лениво, а не жадно.

END_OF_DOCUMENT_MARKER

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

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

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

Quote и макросы

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

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

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

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

require Hygiene
Hygiene.write()
Hygiene.read()
** (CompileError) undefined variable "a" (context Hygiene)

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

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

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

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

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

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

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

Псевдонимы внутри 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

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

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

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

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

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

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

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

defkv [foo: 1, bar: 2]

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

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

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

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

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

Если вы попытаетесь запустить наш новый макрос, вы заметите, что он даже не будет компилироваться, поскольку будет жаловаться, что переменные k и v не существуют. Это связано с неоднозначностью: unquote(k) может быть фрагментом unquote, как раньше, или обычным unquote, как в unquote(kv).

Одним из решений этой проблемы является отключение unquoting в макросе, однако это сделает невозможным внедрение представления kv в дерево. Именно тогда опция :bind_quoted приходит на помощь (снова!). Используя :bind_quoted, мы можем автоматически отключить unquoting, одновременно внедряя необходимые переменные в дерево:

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 рекомендуется всякий раз, когда требуется внедрить значение в quote.

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 . Если обработанное выражение попадает в пункты 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 не будет ожидаемым:

{: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(opts) 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(opts) 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]}

Скачать версию ePub

Создано с помощью ExDoc (v0.34.1) для язык программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/Kernel.SpecialForms.html

Spec-Zone.ru

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