Источник 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__
Возвращает абсолютный путь к каталогу текущего файла в виде бинарного.
- __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}
Создаёт кортеж.
Функции
%struct{}Source
Соответствует или создает структуру.
Структура — это помеченная карта, которая позволяет разработчикам задавать значения по умолчанию для ключей, метки для использования в полиморфных диспетчерах и утверждениях на этапе компиляции.
Структуры обычно определяются с помощью макроса Kernel.defstruct/1:
defmodule User do defstruct name: "john", age: 27 end
Теперь структуру можно создать следующим образом:
%User{}
Под структурой находится просто карта с ключом :__struct__, указывающим на модуль User.
%User{} == %{__struct__: User, name: "john", age: 27}
Поля структуры можно указать при создании структуры:
%User{age: 31}
#=> %{__struct__: User, name: "john", age: 31}
Или также при сопоставлении с образцом для извлечения значений:
%User{age: age} = user
Также доступна операция обновления, специфичная для структур:
%User{user | age: 28}
Преимущества структур заключаются в том, что они проверяют, являются ли заданные ключи частью определенной структуры. Следующий пример завершится ошибкой, так как ключ :full_name отсутствует в структуре User.
%User{full_name: "john doe"}
Приведенный выше синтаксис гарантирует, что заданные ключи являются допустимыми на этапе компиляции, а также гарантирует, что на этапе выполнения аргумент является структурой, в противном случае возникает ошибка BadStructError.
Хотя структуры являются картами, по умолчанию структуры не реализуют ни одного из протоколов, реализованных для карт. Для получения дополнительной информации о том, как структуры могут использоваться с протоколами для полиморфного диспетчера, см. Kernel.defprotocol/2. Также см. Kernel.struct/2 и Kernel.struct!/2 для примеров создания и обновления структур динамически.
Сопоставление с образцом по именам структур
Помимо возможности сопоставления с образцом по полям структур, например:
%User{age: age} = user
Структуры также позволяют сопоставление с образцом по имени структуры:
%struct_name{} = user
struct_name #=> User
Вы также можете присвоить имя структуры _, когда вы хотите проверить, является ли что-то структурой, но вас не интересует его имя:
%_{} = user %{}Source
Создает карту.
Для получения дополнительной информации о картах, их синтаксисе и способах доступа к ним и манипулирования ими см. модуль Map.
Представление AST
Независимо от того, используется ли => или синтаксис ключевых слов, пары ключ-значение в картах всегда представляются внутри как список кортежей из двух элементов для простоты:
iex> quote do
...> %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]} &exprSource
Оператор захвата. Захватывает или создает анонимную функцию.
Захват
Оператор захвата чаще всего используется для захвата функции с заданным именем и арностью из модуля:
iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false
В приведенном примере мы захватили Kernel.is_atom/1 как анонимную функцию и затем вызвали ее.
Оператор захвата также можно использовать для захвата локальных функций, включая закрытые, и импортированных функций, опуская имя модуля:
&local_function/1
См. также Function.capture/3.
Анонимные функции
Оператор захвата также можно использовать для частичного применения функций, где &1, &2 и т. д. могут использоваться в качестве заполнительных значений. Например:
iex> double = &(&1 * 2) iex> double.(2) 4
Другими словами, &(&1 * 2) эквивалентно fn x -> x * 2 end.
Мы можем частично применить удаленную функцию с заполнителем:
iex> take_five = &Enum.take(&1, 5) iex> take_five.(1..10) [1, 2, 3, 4, 5]
Еще один пример с использованием импортированной или локальной функции:
iex> first_elem = &elem(&1, 0)
iex> first_elem.({0, 1})
0
Оператор & может использоваться с более сложными выражениями:
iex> fun = &(&1 + &2 + &3) iex> fun.(1, 2, 3) 6
А также со списками и кортежами:
iex> fun = &{&1, &2}
iex> fun.(1, 2)
{1, 2}
iex> fun = &[&1 | &2]
iex> fun.(1, [2, 3])
[1, 2, 3]
Единственные ограничения при создании анонимных функций заключаются в том, что должно присутствовать по крайней мере один заполнитель, т. е. он должен содержать по крайней мере &1, и что выражения блоков не поддерживаются:
# No placeholder, fails to compile. &(:foo) # Block expression, fails to compile. &(&1; &2)
left . rightSource
Оператор точки. Определяет удаленный вызов, вызов анонимной функции или псевдоним.
Точка (.) в Elixir может использоваться для удаленных вызовов:
iex> String.downcase("FOO")
"foo"
В этом примере мы использовали . для вызова downcase в модуле String, передавая "FOO" в качестве аргумента.
Точка может использоваться для вызова анонимных функций тоже:
iex> (fn n -> n end).(7) 7
в этом случае слева находится функция.
Мы также можем использовать точку для создания псевдонимов:
iex> Hello.World Hello.World
На этот раз мы объединили два псевдонима, определив конечный псевдоним Hello.World.
Синтаксис
Правая часть . может быть словом, начинающимся с заглавной буквы, которое представляет псевдоним, словом, начинающимся со строчной буквы или подчеркивания, любым допустимым языковым оператором или любым именем, заключенным в одинарные или двойные кавычки. Вот несколько примеров:
iex> Kernel.Sample Kernel.Sample iex> Kernel.length([1, 2, 3]) 3 iex> Kernel.+(1, 2) 3 iex> Kernel."+"(1, 2) 3
Заключение имени функции в одинарные или двойные кавычки всегда является удаленным вызовом. Следовательно, Kernel."Foo" будет пытаться вызвать функцию "Foo", а не вернуть псевдоним Kernel.Foo. Это сделано по дизайну, так как имена модулей строже, чем имена функций.
При использовании точки для вызова анонимной функции существует только один операнд, но он все равно записывается с использованием постфиксной нотации:
iex> negate = fn n -> -n end iex> negate.(7) -7
Выражение в кавычках
Когда . используется, выражение в кавычках может иметь две различные формы. Когда правая часть начинается со строчной буквы (или подчеркивания):
iex> quote do
...> String.downcase("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Обратите внимание, что у нас есть внутренний кортеж, содержащий атом :., представляющий точку, как первый элемент:
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
Этот кортеж следует общей структуре выражений в кавычках в Elixir, с именем в качестве первого аргумента, некоторым списком ключевых слов в качестве метаданных, как вторым, и списком аргументов как третьим. В этом случае аргументами являются псевдоним String и атом :downcase. Второй аргумент в удаленном вызове всегда является атомом.
В случае вызовов анонимных функций внутренний кортеж со специальной формой точки имеет только один аргумент, отражая тот факт, что оператор является унарным:
iex> quote do
...> negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}
В случае, когда правая часть является псевдонимом (т. е. начинается с заглавной буквы), мы получаем вместо этого:
iex> quote do
...> Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}
Мы рассматриваем псевдонимы подробнее в документации к специальной форме __aliases__/1.
Раскрытие
Мы также можем использовать раскрытие, чтобы сгенерировать удаленный вызов в выражении в кавычках:
iex> x = :downcase
iex> quote do
...> String.unquote(x)("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Аналогично Kernel."FUNCTION_NAME", unquote(x) всегда будет генерировать удаленный вызов, независимо от значения x. Чтобы сгенерировать псевдоним с помощью выражения в кавычках, необходимо использовать Module.concat/2:
iex> x = Sample
iex> quote do
...> Module.concat(String, unquote(x))
...> end
{{:., [], [{:__aliases__, [alias: false], [:Module]}, :concat]}, [],
[{:__aliases__, [alias: false], [:String]}, Sample]} __aliases__(args)Source
Внутренняя специальная форма для хранения информации о псевдонимах.
Обычно она компилируется в атом:
iex> quote do
...> Foo.Bar
...> end
{:__aliases__, [alias: false], [:Foo, :Bar]}
Elixir представляет Foo.Bar как __aliases__, чтобы вызовы можно было однозначно идентифицировать по оператору :.. Например:
iex> quote do
...> Foo.bar()
...> end
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}
Всякий раз, когда итератор выражений видит :. в качестве ключа кортежа, он может быть уверен, что это вызов, а второй аргумент в списке является атомом.
С другой стороны, псевдонимы имеют некоторые свойства:
Первый элемент псевдонимов может быть любым термином, который должен быть преобразован в атом на этапе компиляции.
Элементы псевдонимов гарантированно всегда являются атомами.
Когда первый элемент псевдонимов является атомом
:Elixir, никакого преобразования не происходит.
__block__(args)Source
Внутренняя специальная форма для выражений блоков.
Это специальная форма, используемая всякий раз, когда у нас есть блок выражений в Elixir. Эта специальная форма является закрытой и не должна вызываться напрямую:
iex> quote do
...> 1
...> 2
...> 3
...> end
{:__block__, [], [1, 2, 3]} __CALLER__Source
Возвращает текущую среду вызова в виде структуры Macro.Env.
В среде вы можете получить доступ к имени файла, номерам строк, настроить псевдонимы, функцию и т. д.
__DIR__Source
Возвращает абсолютный путь к каталогу текущего файла в виде бинарного.
Несмотря на то, что к каталогу можно получить доступ как Path.dirname(__ENV__.file), этот макрос является удобным сокращением.
__ENV__Source
Возвращает информацию о текущей среде в виде структуры Macro.Env.
В среде вы можете получить доступ к текущему имени файла, номерам строк, настроить псевдонимы, текущей функции и т. д.
__MODULE__Source
Возвращает текущее имя модуля в виде атома или nil в противном случае.
Хотя к модулю можно обратиться в __ENV__/0, эта макрокоманда является удобным сокращением.
__STACKTRACE__Source
Возвращает стек вызовов для текущего обрабатываемого исключения.
Доступно только в catch и rescue частях выражений try/1.
Для получения стека вызовов текущего процесса используйте Process.info(self(), :current_stacktrace).
left :: rightSource
Оператор типа. Используется типами и битовыми строками для указания типов.
Этот оператор используется в Elixir в двух различных случаях. Он используется в типах для указания типа переменной, функции или самого типа:
@type number :: integer | float @spec add(number, number) :: number
Он также может использоваться в битовых строках для указания типа заданного битового сегмента:
<<int::integer-little, rest::bits>> = bits
Подробнее ознакомьтесь с документацией по странице Типы и <<>>/1 соответственно для типов и битовых строк.
<<args>>Source
Определяет новую битовую строку.
Примеры
iex> <<1, 2, 3>> <<1, 2, 3>>
Типы
Битовая строка состоит из многих сегментов, и каждый сегмент имеет тип. В битовых строках используются 9 типов:
integerfloat-
bits(псевдоним дляbitstring) bitstringbinary-
bytes(псевдоним дляbinary) utf8utf16utf32
Если тип не указан, по умолчанию используется 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 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, 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
Дополнительную информацию о конкретных оптимизациях и соображениях по производительности см. в главе "Constructing and matching binaries" (Построение и сопоставление бинарных данных) в Руководстве по производительности 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]
Понимание принимает множество генераторов и фильтров. for использует оператор <- для извлечения значений из перечислимого объекта справа и сопоставления их с шаблоном слева. Мы называем их генераторами:
# A list generator: iex> for n <- [1, 2, 3, 4], do: n * 2 [2, 4, 6, 8] # A comprehension with two generators iex> for x <- [1, 2], y <- [2, 3], do: x * y [2, 3, 4, 6]
Также могут быть указаны фильтры:
# A comprehension with a generator and a filter iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n [2, 4, 6]
Фильтры должны вычисляться как истинные значения (всё, кроме nil и false). Если фильтр ложный, то текущее значение отбрасывается.
Генераторы также могут использоваться для фильтрации, так как они удаляют любые значения, не соответствующие шаблону слева от <-.
iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...> String.upcase(name)
...> end
["JOHN", "MEG"]
Также поддерживаются генераторы битовых векторов, которые очень полезны, когда вам нужно организовать потоки битовых векторов:
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
Присваивания переменных внутри понимания, будь то в генераторах, фильтрах или внутри блока, не отображаются за пределами понимания.
Присваивания переменных внутри фильтров всё ещё должны возвращать истинное значение, в противном случае значения отбрасываются. Посмотрим на пример. Представьте себе список ключевых слов, где ключ — это язык программирования, а значение — его непосредственный родитель. Затем давайте попробуем вычислить прародителя каждого языка.
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent = languages[parent], do: {language, grandparent}
[elixir: :prolog]
Учитывая, что прародителями Erlang и Prolog были nil, эти значения были отфильтрованы. Если вы не хотите такого поведения, простым вариантом является перемещение фильтра в блок do:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages do
...> grandparent = languages[parent]
...> {language, grandparent}
...> end
[elixir: :prolog, erlang: nil, prolog: nil]
Однако такой вариант не всегда доступен, так как у вас могут быть дополнительные фильтры. Альтернативой является преобразование фильтра в генератор, обернув правую часть = в список:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent <- [languages[parent]], do: {language, grandparent}
[elixir: :prolog, erlang: nil, prolog: nil]
Опции :into и :uniq
В примерах выше, результат, возвращаемый пониманием, всегда был списком. Возвращаемый результат можно настроить, передав опцию :into, которая принимает любую структуру, если она реализует протокол Collectable.
Например, мы можем использовать генераторы битовых векторов с опцией :into для лёгкого удаления всех пробелов в строке:
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>> "helloworld"
Модуль IO предоставляет потоки, которые являются как Enumerable, так и Collectable. Вот эхо-сервер с преобразованием в верхний регистр, использующий понимания:
for line <- IO.stream(), into: IO.stream() do String.upcase(line) end
Аналогично, uniq: true также можно передать в понимания, чтобы гарантировать, что результаты добавляются в коллекцию только в том случае, если они не были возвращены ранее. Например:
iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2 [2, 4, 6] iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>> "ABC"
Опция :reduce
В то время как опция :into позволяет нам настроить поведение понимания для определённого типа данных, например, поместить все значения в карту или двоичное представление, этого часто недостаточно.
Например, представьте двоичную строку с буквами, где вы хотите подсчитать, сколько раз встречается каждый символ в нижнем регистре, игнорируя все символы в верхнем регистре. Например, для строки "AbCabCABc", мы хотим вернуть карту %{"a" => 1, "b" => 2, "c" => 1}.
Если бы мы использовали :into, нам понадобился бы тип данных, который вычислял бы частоту каждого элемента, который он содержит. Хотя такого типа данных в Elixir нет, вы можете реализовать его самостоятельно.
Проще было бы использовать понимания для сопоставления и фильтрации букв, а затем вызвать Enum.reduce/3 для построения карты, например:
iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}
Хотя вышеупомянутый подход прост, он имеет недостаток — данные необходимо обработать как минимум дважды. Если вы ожидаете длинные строки в качестве входных данных, это может быть довольно затратно.
К счастью, понимания также поддерживают опцию :reduce, которая позволила бы нам объединить оба шага выше в один:
iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...> acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}
Когда указан ключ :reduce, его значение используется в качестве начального аккумулятора, и блок do должен быть изменён на использование -> условий, где левая часть -> получает накопленное значение предыдущей итерации, а выражение справа должно возвращать новое значение аккумулятора. Как только больше нет элементов, возвращается конечное значение аккумулятора. Если элементов вообще нет, возвращается начальное значение аккумулятора.
import(module, opts)Source
Импортирует функции и макросы из других модулей.
import/2 позволяет легко получить доступ к функциям или макросам из других модулей без использования квалифицированного имени.
Примеры
Если вы используете несколько функций из данного модуля, вы можете импортировать эти функции и ссылаться на них как на локальные функции, например:
iex> import List iex> flatten([1, [2], 3]) [1, 2, 3]
Выбор
По умолчанию Elixir импортирует функции и макросы из данного модуля, за исключением тех, которые начинаются с подчёркивания (обычно это колбэки):
import List
Разработчик может отфильтровать импорт, чтобы импортировать только функции, макросы или сигнилы (которые могут быть функциями или макросами), через опцию :only:
import List, only: :functions import List, only: :macros import Kernel, only: :sigils
В качестве альтернативы, Elixir позволяет разработчику передавать пары имя/арность в :only или :except для точного управления тем, что импортировать (или не импортировать):
import List, only: [flatten: 1] import String, except: [split: 2]
Повторный импорт того же модуля стёрнет предыдущие импорты, за исключением случая использования опции except, которая всегда является исключительной для ранее объявленного import/2. Если предыдущего импорта нет, то она применяется ко всем функциям и макросам в модуле. Например:
import List, only: [flatten: 1, keyfind: 4] import List, except: [flatten: 1]
После двух вызовов импорта будет импортирована только List.keyfind/4.
Функции с подчёркиванием
По умолчанию функции, начинающиеся с _, не импортируются. Если вам действительно нужно импортировать функцию, начинающуюся с _, вы должны явно включить её в :only выбор.
import File.Stream, only: [__build__: 3]
Лексическая область видимости
Важно отметить, что import/2 является лексической. Это означает, что вы можете импортировать определённые макросы внутри определённых функций:
defmodule Math do
def some_function do
# 1) Disable "if/2" from Kernel
import Kernel, except: [if: 2]
# 2) Require the new "if/2" macro from MyMacros
import MyMacros
# 3) Use the new macro
if do_something, it_works
end
end
В примере выше, мы импортировали макросы из MyMacros, заменяя оригинальную реализацию if/2 своей внутри этой конкретной функции. Все остальные функции в этом модуле по-прежнему смогут использовать оригинальную.
Предупреждения
Если вы импортируете модуль и не используете ни одну из импортированных функций или макросов из этого модуля, Elixir выведет предупреждение, подразумевая, что импорт не используется.
В случае, если импорт генерируется автоматически макросом, Elixir не будет выдавать предупреждений, так как импорт не был явно определён.
Оба варианта поведения предупреждений могут быть изменены, явно установив опцию :warn в true или false.
Неоднозначные имена функций/макросов
Если два модуля A и B импортированы и оба содержат функцию foo с арностью 1, ошибка генерируется только в том случае, если фактически выполняется неоднозначный вызов foo/1; то есть ошибки генерируются лениво, а не жадно.
quote(opts, block)Source
Получение представления любого выражения.
Примеры
iex> quote do
...> sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}
Абстрактное синтаксическое дерево (AST) Elixir
Любой код Elixir может быть представлен с помощью структур данных Elixir. Основной строительный блок макросов Elixir — кортеж из трёх элементов, например:
{:sum, [], [1, 2, 3]}
Этот кортеж представляет собой вызов функции sum с аргументами 1, 2 и 3. Элементы кортежа:
Первый элемент кортежа всегда атом или другой кортеж с таким же представлением.
Второй элемент кортежа представляет метаданные.
Третий элемент кортежа — аргументы для вызова функции. Третий аргумент может быть атомом, который обычно является переменной (или локальным вызовом).
Помимо описанного кортежа, Elixir имеет несколько литералов, которые также являются частью его AST. Эти литералы возвращают сами себя при использовании quote. Они:
: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 обычно используется с макросами для генерации кода. В качестве упражнения, давайте определим макрос, который возводит число в квадрат.
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.()
Что вызывает функцию дважды, объясняя, почему мы получаем значение дважды! В большинстве случаев это нежелательное поведение, поэтому нужно помнить, что нельзя unquote одного и того же значения более одного раза.
Исправим наш макрос:
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 сохранит определение файла текущей позиции, но unquoted аргументы могут содержать информацию о строке вызывающей стороны макроса, что приведёт к ошибочным трассировкам стека.
Привязки и фрагменты 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- процесс должен неограниченно ждать совпадающего сообщения, это то же самое, что и не использование after0- если в почтовом ящике нет совпадающего сообщения, таймаут наступит немедленноположительное целое число, меньшее или равное
4_294_967_295(0xFFFFFFFFв шестнадцатеричной записи) - значение времени ожидания должно быть представимо как беззнаковое 32-битное целое число.
Обработка переменных
Специальная форма receive/1 обрабатывает переменные точно так же, как и специальный макрос case/2. Для получения дополнительной информации обратитесь к документации для case/2.
require(module, opts)Source
Использует модуль для использования его макросов.
Примеры
Общедоступные функции в модулях глобально доступны, но для использования макросов необходимо явно указать модуль, в котором они определены.
Предположим, что вы создали собственную реализацию if/2 в модуле MyMacros. Если вы хотите вызвать её, вам необходимо сначала явно загрузить модуль MyMacros:
defmodule Math do require MyMacros MyMacros.if do_something, it_works end
Попытка вызвать макрос, который не загружен, приведёт к ошибке.
Сокращение псевдонима
require/2 также принимает :as в качестве опции, чтобы автоматически создать псевдоним. Для получения дополнительной информации см. alias/2.
super(args)Source
Вызывает переопределённую функцию при переопределении с помощью Kernel.defoverridable/1.
См. Kernel.defoverridable/1 для получения дополнительной информации и документации.
try(args)Source
Вычисляет заданные выражения и обрабатывает любые ошибки, выходы или броски, которые могли произойти.
Примеры
try do
do_something_that_may_fail(some_arg)
rescue
ArgumentError ->
IO.puts("Invalid argument given")
catch
value ->
IO.puts("Caught #{inspect(value)}")
else
value ->
IO.puts("Success! The result was #{inspect(value)}")
after
IO.puts("This is printed regardless if it failed or succeeded")
end
rescue используется для обработки исключений, а catch используется для перехвата брошенных значений и выходов. else используется для управления потоком на основе результата выражения. catch, rescue, и else строки работают на основе сопоставления с образцом (аналогично специальной форме case).
Вызовы внутри try/1 не являются рекурсивными, так как виртуальной машине необходимо сохранять стек-трейс на случай возникновения исключения. Чтобы получить стек-трейс, обратитесь к __STACKTRACE__/0 внутри rescue или catch строк.
Строки rescue
Помимо использования сопоставления с образцом, строки rescue предоставляют удобства работы с исключениями, которые позволяют перехватывать исключение по его имени. Все следующие форматы являются допустимыми шаблонами в строках rescue:
# Rescue a single exception without binding the exception # to a variable try do UndefinedModule.undefined_function rescue UndefinedFunctionError -> nil end # Rescue any of the given exception without binding try do UndefinedModule.undefined_function rescue [UndefinedFunctionError, ArgumentError] -> nil end # Rescue and bind the exception to the variable "x" try do UndefinedModule.undefined_function rescue x in [UndefinedFunctionError] -> nil end # Rescue all kinds of exceptions and bind the rescued exception # to the variable "x" try do UndefinedModule.undefined_function rescue x -> nil end
Ошибки Erlang
Ошибки Erlang преобразуются в ошибки Elixir при перехвате:
try do :erlang.error(:badarg) rescue ArgumentError -> :ok end #=> :ok
Наиболее распространённые ошибки Erlang будут преобразованы в их аналоги в Elixir. Те, которые не будут, преобразуются в более общую ErlangError:
try do :erlang.error(:unknown) rescue ErlangError -> :ok end #=> :ok
Фактически, ErlangError можно использовать для перехвата любой ошибки, которая не является соответствующей ошибкой Elixir. Например, она может быть использована для перехвата ранее упомянутой ошибки :badarg до преобразования:
try do :erlang.error(:badarg) rescue ErlangError -> :ok end #=> :ok
Строки catch
Строка catch может быть использована для перехвата брошенных значений, выходов и ошибок.
Перехват брошенных значений
catch можно использовать для перехвата значений, брошенных с помощью Kernel.throw/1:
try do
throw(:some_value)
catch
thrown_value ->
IO.puts("A value was thrown: #{inspect(thrown_value)}")
end
Перехват значений любого типа
Строка catch также поддерживает перехват выходов и ошибок. Для этого она позволяет сопоставлять как тип перехваченного значения, так и само значение:
try do
exit(:shutdown)
catch
:exit, value ->
IO.puts("Exited with value #{inspect(value)}")
end
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts("Caught exit or throw with value #{inspect(value)}")
end
Строка catch также поддерживает :error наряду с :exit и :throw, как и в Erlang, хотя это обычно избегается в пользу raise/rescue механизмов управления. Одна из причин этого заключается в том, что при перехвате :error, ошибка не преобразуется автоматически в ошибку Elixir:
try do :erlang.error(:badarg) catch :error, :badarg -> :ok end #=> :ok
Строки after
Строка after позволяет определить логику очистки, которая будет вызвана как при успешном выполнении блока кода, переданного в try/1, так и при возникновении ошибки. Обратите внимание, что процесс выйдет как обычно при получении сигнала выхода, который вызывает его резкий выход, и поэтому строка after не гарантируется для выполнения. К счастью, большинство ресурсов в Elixir (такие как открытые файлы, таблицы ETS, порты, сокеты и т. д.) связаны с или отслеживают владеющий процесс и автоматически очистятся, если этот процесс выйдет.
File.write!("tmp/story.txt", "Hello, World")
try do
do_something_with("tmp/story.txt")
after
File.rm("tmp/story.txt")
end
Хотя строки after вызываются независимо от наличия ошибки, они не изменяют возвращаемое значение. Все следующие примеры возвращают :return_me:
try do
:return_me
after
IO.puts("I will be printed")
:not_returned
end
try do
raise "boom"
rescue
_ -> :return_me
after
IO.puts("I will be printed")
:not_returned
end
Строки else
Строки else позволяют сопоставить результат тела, переданного в try/1:
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end
Если строка else отсутствует, и не возникает никаких исключений, будет возвращено значение выражения:
x = 1
^x =
try do
1 / x
rescue
ArithmeticError ->
:infinity
end
Однако, если строка else присутствует, но результат выражения не соответствует ни одному из шаблонов, будет поднято исключение. Это исключение не будет перехвачено строкой catch или rescue в той же try:
x = 1
try do
try do
1 / x
rescue
# The TryClauseError cannot be rescued here:
TryClauseError ->
:error_a
else
0 ->
:small
end
rescue
# The TryClauseError is rescued here:
TryClauseError ->
:error_b
end
Аналогично, исключение внутри строки else не перехватывается или не обрабатывается внутри той же try:
try do
try do
nil
catch
# The exit(1) call below can not be caught here:
:exit, _ ->
:exit_a
else
_ ->
exit(1)
end
catch
# The exit is caught here:
:exit, _ ->
:exit_b
end
Это означает, что виртуальная машина больше не должна сохранять стек-трейс, когда попадает внутрь строки else , и поэтому хвостовая рекурсия возможна при использовании try с хвостовым вызовом в качестве конечного вызова внутри строки else . То же самое верно для rescue и catch строк.
Только результат проведённого выражения передаётся в строку else . Если try оканчивается в rescue или catch строках, их результат не передаётся в else:
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end
Обработка переменных
Поскольку выражение внутри try может не быть вычислено из-за исключения, любая переменная, созданная внутри try , не может быть обработана извне. Например:
try do x = 1 do_something_that_may_fail(same_arg) :ok catch _, _ -> :failed end x #=> unbound variable "x"
В приведённом примере x недоступно, так как оно было определено внутри строки try . Общим способом решения этой проблемы является возврат переменных, определённых внутри try:
x =
try do
x = 1
do_something_that_may_fail(same_arg)
x
catch
_, _ -> :failed
end unquote(expr)Source
Разумно извлекает данное выражение внутри цитируемого выражения.
Эта функция ожидает допустимого Elixir AST, также известного как цитируемое выражение, в качестве аргумента. Если вы хотите 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]}
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/Kernel.SpecialForms.html