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>>
Двоичные данные необходимо явно пометить как binary:
iex> rest = "oo" iex> <<102, rest::binary>> "foo"
Типы utf8, utf16, и utf32 предназначены для кодов Юникода. Они также могут применяться к литеральным строкам и спискам символов:
iex> <<"foo"::utf16>> <<0, 102, 0, 111, 0, 111>> iex> <<"foo"::utf32>> <<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
В противном случае при построении двоичных данных будет получено исключение ArgumentError:
rest = "oo" <<102, rest>> ** (ArgumentError) argument error
Параметры
Многие параметры можно задать, используя - в качестве разделителя. Порядок произвольный, поэтому следующие варианты эквивалентны:
<<102::integer-native, rest::binary>> <<102::native-integer, rest::binary>> <<102::unsigned-big-integer, rest::binary>> <<102::unsigned-big-integer-size(8), rest::binary>> <<102::unsigned-big-integer-8, rest::binary>> <<102::8-integer-big-unsigned, rest::binary>> <<102, rest::binary>>
Единица и размер
Длина совпадения равна unit (количество битов) умноженному на size (количество повторяющихся сегментов длиной unit).
| Тип | Единица по умолчанию |
|---|---|
integer |
1 бит |
float |
1 бит |
binary |
8 бит |
Размеры для типов несколько сложнее. По умолчанию размер целых чисел равен 8.
Для чисел с плавающей точкой он равен 64. Для чисел с плавающей точкой size * unit должно быть равно 16, 32 или 64, что соответствует IEEE 754 binary16, binary32 и binary64 соответственно.
Для двоичных данных по умолчанию используется размер двоичных данных. Только последний двоичный массив может использовать размер по умолчанию. Все остальные должны иметь свой размер, указанный явно, даже если совпадение однозначно. Например:
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}
Размер может быть переменной или любым допустимым выражением условия:
iex> name_size = 5
iex> <<name::binary-size(^name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
Размер может использовать предыдущие переменные, определённые в самой битовой строке/двоичных данных:
iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
Однако он не может использовать переменные, определенные в совпадении за пределами битовой строки/двоичных данных:
{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment
Если размер не указан для не последнего элемента, компиляция завершится ошибкой:
<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">> ** (CompileError): a binary field without size is only allowed at the end of a binary pattern
Сокращенная запись
Размер и единица также могут быть указаны с использованием сокращенного синтаксиса при передаче целочисленных значений:
iex> x = 1 iex> <<x::8>> == <<x::size(8)>> true iex> <<x::8*4>> == <<x::size(8)-unit(4)>> true
Этот синтаксис отражает тот факт, что эффективный размер определяется умножением размера на единицу.
Модификаторы
Некоторые типы имеют связанные модификаторы, чтобы устранить неоднозначность в представлении байтов.
| Модификатор | Применимые типы |
|---|---|
signed |
integer |
unsigned (по умолчанию) |
integer |
little |
integer, float, utf16, utf32
|
big (по умолчанию) |
integer, float, utf16, utf32
|
native |
integer, utf16, utf32
|
Знак
Целые числа могут быть signed или unsigned, по умолчанию unsigned.
iex> <<int::integer>> = <<-100>> <<156>> iex> int 156 iex> <<int::integer-signed>> = <<-100>> <<156>> iex> int -100
signed и unsigned используются только для сопоставления двоичных данных (см. ниже) и только для целых чисел.
iex> <<-100::signed, _rest::binary>> = <<-100, "foo">> <<156, 102, 111, 111>>
Порядок байтов
В Elixir есть три варианта порядка байтов: big, little, и native. По умолчанию big:
iex> <<number::little-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 256 iex> <<number::big-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 1
native определяется виртуальной машиной при запуске и зависит от операционной системы хоста.
Сопоставление двоичных/битовых строк
Сопоставление двоичных данных — мощная функция в Elixir, полезная для извлечения информации из двоичных данных, а также для сопоставления шаблонов.
Сопоставление двоичных данных может использоваться самостоятельно для извлечения информации из двоичных данных:
iex> <<"Hello, ", place::binary>> = "Hello, World" "Hello, World" iex> place "World"
Или как часть определения функций для сопоставления шаблонов:
defmodule ImageTyper do
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, _rest::binary>>), do: :png
def type(<<@jpg_signature, _rest::binary>>), do: :jpg
def type(_), do: :unknown
end
Производительность и оптимизации
Компилятор Erlang может обеспечивать ряд оптимизаций при создании и сопоставлении двоичных данных. Чтобы увидеть вывод оптимизации, установите параметр компилятора bin_opt_info:
ERL_COMPILER_OPTIONS=bin_opt_info mix compile
Дополнительную информацию о конкретных оптимизациях и вопросах производительности см. в "Constructing and matching binaries" chapter of the Erlang's Efficiency Guide.
left = rightSource
Оператор сопоставления. Сопоставляет значение справа с шаблоном слева.
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. Эти литералы возвращают себя при цитировании. Это:
:sum #=> Atoms
1 #=> Integers
2.0 #=> Floats
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
Любое другое значение, например, карта или кортеж из четырёх элементов, должно быть экранировано (Macro.escape/1) перед вводом в AST.
Параметры
:bind_quoted— передаёт привязку макросу. Когда задаётся привязка,unquote/1автоматически отключается.:context— устанавливает контекст разрешения.:generated— помечает данный фрагмент как сгенерированный, чтобы не выводить предупреждения. Это также полезно для предотвращения сообщений об ошибках dialyzer при генерации макросами неиспользуемых клаузул.:file— устанавливает для цитируемых выражений указанный файл.:line— устанавливает для цитируемых выражений указанную строку.:location— при значении:keep, сохраняет текущую строку и файл из цитирования. Подробности см. в разделе «Информация об отслеживании стека» ниже.-
:unquote— при значенииfalse, отключает разыменование. Это означает, что любой вызовunquoteбудет сохранён в AST как есть, а не заменён аргументамиunquote. Например:iex> quote do ...> unquote("hello") ...> end "hello" iex> quote unquote: false do ...> unquote("hello") ...> end {:unquote, [], ["hello"]}
Цитирование и макросы
quote/2 обычно используется с макросами для генерации кода. В качестве упражнения давайте определим макрос, который возводит число в квадрат. На практике нет необходимости определять такой макрос (и это, фактически, плохая практика), но он достаточно прост, чтобы мы могли сосредоточиться на важных аспектах цитирования и макросов:
defmodule Math do
defmacro squared(x) do
quote do
unquote(x) * unquote(x)
end
end
end
Его можно вызвать так:
import Math
IO.puts("Got #{squared(5)}")
Сначала в этом примере ничего не указывает на то, что это макрос. Но происходит следующее: во время компиляции squared(5) превращается в 5 * 5. Аргумент 5 дублируется в сгенерированном коде. Мы можем увидеть это на практике, так как наш макрос содержит ошибку:
import Math
my_number = fn ->
IO.puts("Returning 5")
5
end
IO.puts("Got #{squared(my_number.())}")
В приведённом примере будет выведено:
Returning 5 Returning 5 Got 25
Обратите внимание, что «Возвращаемое значение 5» было выведено дважды, а не один раз. Это происходит потому, что макрос получает выражение, а не значение (что мы ожидали бы от обычной функции). Это означает, что:
squared(my_number.())
В действительности расширяется до:
my_number.() * my_number.()
Что вызывает функцию дважды, объясняя, почему мы видим выведенное значение дважды! В большинстве случаев такое поведение не ожидается, и это одна из первых вещей, которые следует учитывать при работе с макросами: не разыменовывать одно и то же значение более одного раза.
Исправим наш макрос:
defmodule Math do
defmacro squared(x) do
quote do
x = unquote(x)
x * x
end
end
end
Теперь вызов squared(my_number.()) как и раньше выведет значение только один раз.
На самом деле, эта модель настолько распространена, что в большинстве случаев вы захотите использовать параметр bind_quoted с quote/2:
defmodule Math do
defmacro squared(x) do
quote bind_quoted: [x: x] do
x * x
end
end
end
:bind_quoted будет переводиться в тот же код, что и в примере выше. :bind_quoted может быть использован во многих случаях и считается хорошей практикой не только потому, что он помогает предотвратить распространённые ошибки, но и потому, что он позволяет использовать другие инструменты, предоставляемые макросами, такие как фрагменты разыменования, обсуждаемые в некоторых разделах ниже.
Прежде чем закончить это краткое введение, вы заметите, что, хотя мы определили переменную x в нашем цитировании:
quote do x = unquote(x) x * x end
При вызове:
import Math squared(5) x ** (CompileError) undefined variable "x"
Мы видим, что x не попал в пользовательский контекст. Это происходит потому, что макросы Elixir — гигиеничные, и мы более подробно рассмотрим этот вопрос в следующих разделах.
Гигиена переменных
Рассмотрим следующий пример:
defmodule Hygiene do
defmacro no_interference do
quote do
a = 1
end
end
end
require Hygiene
a = 10
Hygiene.no_interference()
a
#=> 10
В приведённом примере a возвращает 10, даже если макрос, по-видимому, устанавливает его в 1, поскольку переменные, определённые в макросе, не влияют на контекст выполнения макроса. Если вы хотите установить или получить переменную в контексте вызывающего макрос, вы можете сделать это с помощью макроса var!:
defmodule NoHygiene do
defmacro interference do
quote do
var!(a) = 1
end
end
end
require NoHygiene
a = 10
NoHygiene.interference()
a
#=> 1
Вы не можете даже получить доступ к переменным, определённым в том же модуле, если не укажете явно контекст:
defmodule Hygiene do
defmacro write do
quote do
a = 1
end
end
defmacro read do
quote do
a
end
end
end
Hygiene.write()
Hygiene.read()
** (RuntimeError) undefined variable a or undefined function a/0
Для этого вы можете явно передать текущий контекст модуля как аргумент:
defmodule ContextHygiene do
defmacro write do
quote do
var!(a, ContextHygiene) = 1
end
end
defmacro read do
quote do
var!(a, ContextHygiene)
end
end
end
ContextHygiene.write()
ContextHygiene.read()
#=> 1
Контексты переменных идентифицируются третьим элементом кортежа. По умолчанию контекст равен nil, а quote назначает другой контекст всем переменным внутри:
quote(do: var)
#=> {:var, [], Elixir}
В случае переменных, возвращённых макросами, в метаданных также может быть ключ :counter, который используется для дальнейшей уточнения контекстов и обеспечения изоляции между вызовами макросов, как показано в предыдущем примере.
Гигиена псевдонимов
Псевдонимы внутри цитирования по умолчанию гигиеничны. Рассмотрим следующий пример:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
Hygiene.no_interference()
#=> %{}
Обратите внимание, что, хотя псевдоним M недоступен в контексте расширения макроса, приведённый код работает, поскольку M всё ещё расширяется до Map.
Аналогично, даже если мы определим псевдоним с тем же именем перед вызовом макроса, это не повлияет на результат макроса:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{}
В некоторых случаях вы хотите получить доступ к псевдониму или модулю, определённому в вызывающей функции. Для этого можно использовать макрос alias!:
defmodule Hygiene do
# This will expand to Elixir.Nested.hello()
defmacro no_interference do
quote do
Nested.hello()
end
end
# This will expand to Nested.hello() for
# whatever is Nested in the caller
defmacro interference do
quote do
alias!(Nested).hello()
end
end
end
defmodule Parent do
defmodule Nested do
def hello, do: "world"
end
require Hygiene
Hygiene.no_interference()
** (UndefinedFunctionError) ...
Hygiene.interference()
#=> "world"
end
Гигиена импортов
Аналогично псевдонимам, импорты в Elixir по умолчанию гигиеничны. Рассмотрим следующий код:
defmodule Hygiene do
defmacrop get_length do
quote do
length([1, 2, 3])
end
end
def return_length do
import Kernel, except: [length: 1]
get_length
end
end
Hygiene.return_length()
#=> 3
Обратите внимание, что Hygiene.return_length/0 возвращает 3, даже если функция Kernel.length/1 не импортирована. Фактически, даже если return_length/0 импортировал функцию с тем же именем и арностью из другого модуля, это не повлияет на результат функции:
def return_length do import String, only: [length: 1] get_length end
Вызов этого нового return_length/0 по-прежнему вернёт 3.
Elixir достаточно умён, чтобы отложить разрешение до последнего момента. Таким образом, если вы вызываете length([1, 2, 3]) в цитировании, но функция length/1 недоступна, она расширяется в вызывающей функции:
defmodule Lazy do
defmacrop get_length do
import Kernel, except: [length: 1]
quote do
length("hello")
end
end
def return_length do
import Kernel, except: [length: 1]
import String, only: [length: 1]
get_length
end
end
Lazy.return_length()
#=> 5
Информация об отслеживании стека
При определении функций с помощью макросов разработчики могут выбрать, будут ли ошибки времени выполнения сообщаться из вызывающей функции или из цитирования. Рассмотрим пример:
# adder.ex
defmodule Adder do
@doc "Defines a function that adds two numbers"
defmacro defadd do
quote location: :keep do
def add(a, b), do: a + b
end
end
end
# sample.ex
defmodule Sample do
import Adder
defadd
end
require Sample
Sample.add(:one, :two)
** (ArithmeticError) bad argument in arithmetic expression
adder.ex:5: Sample.add/2
При использовании location: :keep и при передаче некорректных аргументов в Sample.add/2, информация об отслеживании стека будет указывать на файл и строку внутри цитирования. Без location: :keep ошибка сообщается там, где defadd был вызван. location: :keep влияет только на определения внутри цитирования.
location: :keepи разыменованиеНе используйте
location: :keep, если определение функции такжеunquoteнекоторые аргументы макроса. В противном случае Elixir сохранит определение файла текущего расположения, но аргументы разыменования могут содержать информацию о строке из вызывающего макрос, что приведёт к ошибочным отслеживаниям стека.
Привязка и фрагменты разыменования
Механизмы цитирования/разыменования Elixir предоставляют функциональность, называемую фрагментами разыменования. Фрагменты разыменования позволяют легко генерировать функции на лету. Рассмотрим этот пример:
kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
В приведённом примере мы динамически сгенерировали функции foo/0 и bar/0. Теперь представьте, что мы хотим преобразовать эту функциональность в макрос:
defmacro defkv(kv) do
Enum.map(kv, fn {k, v} ->
quote do
def unquote(k)(), do: unquote(v)
end
end)
end
Мы можем вызвать этот макрос так:
defkv [foo: 1, bar: 2]
Однако мы не можем вызвать его так:
kv = [foo: 1, bar: 2] defkv kv
Это происходит потому, что макрос ожидает, что его аргументы будут списком ключевых слов на стадии **компиляции**. Так как в примере выше мы передаём представление переменной kv, наш код терпит неудачу.
Это на самом деле распространённая ловушка при разработке макросов. Мы предполагаем определённую форму в макросе. Мы можем обойти эту проблему, разыменовав переменную внутри цитируемого выражения:
defmacro defkv(kv) do
quote do
Enum.each(unquote(kv), fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
Если вы попытаетесь запустить наш новый макрос, вы заметите, что он даже не скомпилируется, сообщив об ошибке, что переменные k и v не существуют. Это связано с неоднозначностью: unquote(k) может быть либо фрагментом разыменования, как прежде, либо обычным разыменованием, как в unquote(kv).
Одним из решений этой проблемы является отключение разыменования в макросе, однако это сделало бы невозможным встраивание представления kv в дерево. В этом случае параметр :bind_quoted приходит на помощь (опять же!). Используя :bind_quoted, мы можем автоматически отключить разыменование, всё ещё встраивая желаемые переменные в дерево:
defmacro defkv(kv) do
quote bind_quoted: [kv: kv] do
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
На самом деле, параметр :bind_quoted рекомендуется каждый раз, когда необходимо встроить значение в цитирование.
receive(args)Source
Проверяет, есть ли сообщение, соответствующее заданным условиям, в почтовом ящике текущего процесса.
В случае отсутствия такого сообщения текущий процесс приостанавливается, пока не придёт сообщение или не истечёт заданное время ожидания.
Примеры
receive do
{:selector, number, name} when is_integer(number) ->
name
name when is_atom(name) ->
name
_ ->
IO.puts(:stderr, "Unexpected message received")
end
Необязательное after условие может быть указано в случае, если сообщение не было получено после заданного интервала времени, указанного в миллисекундах:
receive do
{:selector, number, name} when is_integer(number) ->
name
name when is_atom(name) ->
name
_ ->
IO.puts(:stderr, "Unexpected message received")
after
5000 ->
IO.puts(:stderr, "No message in 5 seconds")
end
Условие after может быть указано, даже если нет условий сопоставления. Значение таймаута, заданное для after может быть любым выражением, вычисляемым в одно из допустимых значений:
:infinity— процесс должен бесконечно ждать соответствующего сообщения, это то же самое, что и не использование after-условия0— если в почтовом ящике нет соответствующего сообщения, таймаут произойдёт немедленноположительное целое число, меньшее или равное
4_294_967_295(0xFFFFFFFFв шестнадцатеричной нотации) — значение таймаута должно быть представимо как беззнаковое 32-битное целое число.
Обработка переменных
Специальный оператор receive/1 обрабатывает переменные точно так же, как и специальный макрос case/2. Более подробную информацию можно найти в документации для case/2.
require(module, opts)Source
Требует модуль для использования его макросов.
Примеры
Общедоступные функции в модулях доступны глобально, но для использования макросов вам нужно явно потребовать модуль, в котором они определены.
Предположим, вы создали свою собственную реализацию if/2 в модуле MyMacros. Если вы хотите вызвать её, вам нужно сначала явно потребовать модуль MyMacros:
defmodule Math do require MyMacros MyMacros.if do_something, it_works end
Попытка вызвать макрос, который не был загружен, приведёт к ошибке.
Сокращение псевдонима
require/2 также принимает :as в качестве опции, чтобы автоматически установить псевдоним. Пожалуйста, обратитесь к alias/2 за дополнительной информацией.
super(args)Source
Вызывает переопределённую функцию при её переопределении с помощью Kernel.defoverridable/1.
См. Kernel.defoverridable/1 для получения дополнительной информации и документации.
try(args)Source
Вычисляет заданные выражения и обрабатывает любые ошибки, выходы или броски, которые могут произойти.
Примеры
try do
do_something_that_may_fail(some_arg)
rescue
ArgumentError ->
IO.puts("Invalid argument given")
catch
value ->
IO.puts("Caught #{inspect(value)}")
else
value ->
IO.puts("Success! The result was #{inspect(value)}")
after
IO.puts("This is printed regardless if it failed or succeeded")
end
Условие rescue используется для обработки исключений, а условие catch может быть использовано для перехвата брошенных значений и выходов. Условие else может быть использовано для управления потоком на основе результата выражения. catch, rescue, и else условия работают на основе сопоставления с образцом (аналогично специальному оператору case).
Вызовы внутри try/1 не являются хвостовыми рекурсиями, так как ВМ необходимо сохранить стек вызовов на случай возникновения исключения. Чтобы получить стек вызовов, обратитесь к __STACKTRACE__/0 внутри условия rescue или catch.
rescue условия
Помимо использования сопоставления с образцом, rescue условия предоставляют некоторые удобства для работы с исключениями, которые позволяют перехватывать исключение по его имени. Все следующие форматы являются допустимыми шаблонами в rescue условиях:
# Rescue a single exception without binding the exception # to a variable try do UndefinedModule.undefined_function rescue UndefinedFunctionError -> nil end # Rescue any of the given exception without binding try do UndefinedModule.undefined_function rescue [UndefinedFunctionError, ArgumentError] -> nil end # Rescue and bind the exception to the variable "x" try do UndefinedModule.undefined_function rescue x in [UndefinedFunctionError] -> nil end # Rescue all kinds of exceptions and bind the rescued exception # to the variable "x" try do UndefinedModule.undefined_function rescue x -> nil end
Ошибки Erlang
Ошибки Erlang преобразуются в ошибки Elixir при перехвате:
try do :erlang.error(:badarg) rescue ArgumentError -> :ok end #=> :ok
Наиболее распространённые ошибки Erlang будут преобразованы в их аналоги в Elixir. Те, которые не будут преобразованы, будут преобразованы в более общую ErlangError:
try do :erlang.error(:unknown) rescue ErlangError -> :ok end #=> :ok
Фактически, ErlangError может быть использована для перехвата любых ошибок, которые не являются собственными ошибками Elixir. Например, она может быть использована для перехвата предыдущей :badarg ошибки до преобразования:
try do :erlang.error(:badarg) rescue ErlangError -> :ok end #=> :ok
catch условия
Условие catch может быть использовано для перехвата брошенных значений, выходов и ошибок.
Перехват брошенных значений
catch может быть использовано для перехвата значений, брошенных с помощью Kernel.throw/1:
try do
throw(:some_value)
catch
thrown_value ->
IO.puts("A value was thrown: #{inspect(thrown_value)}")
end
Перехват значений любого типа
Условие catch также поддерживает перехват выходов и ошибок. Для этого оно позволяет сопоставлять как тип перехваченного значения, так и само значение:
try do
exit(:shutdown)
catch
:exit, value ->
IO.puts("Exited with value #{inspect(value)}")
end
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts("Caught exit or throw with value #{inspect(value)}")
end
Условие catch также поддерживает :error наряду с :exit и :throw, как в Erlang, хотя это обычно избегается в пользу механизмов управления raise/rescue. Одна из причин этого заключается в том, что при перехвате :error, ошибка не преобразуется автоматически в ошибку Elixir:
try do :erlang.error(:badarg) catch :error, :badarg -> :ok end #=> :ok
after условия
Условие after позволяет определить логику очистки, которая будет вызвана как при успешном выполнении блока кода, переданного try/1, так и при возникновении ошибки. Обратите внимание, что процесс выйдет как обычно при получении сигнала выхода, вызывающего его внезапное завершение, и поэтому выполнение условия after не гарантируется. К счастью, большинство ресурсов в Elixir (например, открытые файлы, таблицы ETS, порты, сокеты и т. д.) связаны с владельческим процессом или отслеживают его, и автоматически очищаются, если этот процесс завершается.
File.write!("tmp/story.txt", "Hello, World")
try do
do_something_with("tmp/story.txt")
after
File.rm("tmp/story.txt")
end
Хотя after условия вызываются независимо от того, произошла ли ошибка, они не изменяют возвращаемое значение. Все следующие примеры возвращают :return_me:
try do
:return_me
after
IO.puts("I will be printed")
:not_returned
end
try do
raise "boom"
rescue
_ -> :return_me
after
IO.puts("I will be printed")
:not_returned
end
else условия
else условия позволяют сопоставить результат тела, переданного try/1:
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end
Если else условие отсутствует, и исключения не генерируются, то возвращается результат выражения:
x = 1
^x =
try do
1 / x
rescue
ArithmeticError ->
:infinity
end
Однако, когда else условие присутствует, но результат выражения не соответствует ни одному из шаблонов, то будет возбуждено исключение. Это исключение не будет перехвачено catch или rescue в том же try:
x = 1
try do
try do
1 / x
rescue
# The TryClauseError cannot be rescued here:
TryClauseError ->
:error_a
else
0 ->
:small
end
rescue
# The TryClauseError is rescued here:
TryClauseError ->
:error_b
end
Аналогично, исключение внутри else условия не перехватывается или обрабатывается внутри того же try:
try do
try do
nil
catch
# The exit(1) call below can not be caught here:
:exit, _ ->
:exit_a
else
_ ->
exit(1)
end
catch
# The exit is caught here:
:exit, _ ->
:exit_b
end
Это означает, что ВМ больше не нужно сохранять стек вызовов, как только он попадает в else условие, и поэтому хвостовая рекурсия возможна при использовании try с хвостовым вызовом в качестве последнего вызова внутри else условия. То же самое относится к rescue и catch условиям.
Только результат вычисленного выражения передаётся в else условие. Если try оказывается в rescue или catch условиях, их результат не будет передан в else:
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end
Обработка переменных
Поскольку выражение внутри try может не быть вычислено из-за исключения, любая переменная, созданная внутри try не может быть обращена к ней извне. Например:
try do x = 1 do_something_that_may_fail(same_arg) :ok catch _, _ -> :failed end x #=> unbound variable "x"
В приведённом примере x не может быть обращена к ней извне, так как она была определена внутри try условия. Распространённый подход для решения этой проблемы заключается в возвращении переменных, определённых внутри try:
x =
try do
x = 1
do_something_that_may_fail(same_arg)
x
catch
_, _ -> :failed
end unquote(expr)Source
Раскрывает данное выражение внутри выражения с цитированием.
Эта функция ожидает допустимый AST Elixir, также известный как цитируемое выражение, в качестве аргумента. Если вы хотите unquote любое значение, такое как карта или четырёхэлементная кортеж, вы должны вызвать Macro.escape/1 перед раскрытием.
Примеры
Представьте ситуацию, когда у вас есть цитируемое выражение, и вы хотите вставить его внутри другого цитируемого выражения. Первая попытка будет:
value =
quote do
13
end
quote do
sum(1, value, 3)
end
В этом случае аргумент для вызова функции :sum не является ожидаемым результатом:
{:sum, [], [1, {:value, [], Elixir}, 3]}
Для этого мы используем unquote:
iex> value =
...> quote do
...> 13
...> end
iex> quote do
...> sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}
Если вы хотите раскрыть значение, которое не является цитируемым выражением, например, карту, вам нужно вызвать Macro.escape/1 предварительно:
iex> value = %{foo: :bar}
iex> quote do
...> process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}
Если вы забудете это сделать, Elixir выдаст ошибку при компиляции кода.
unquote_splicing(expr)Source
Раскрывает данный список, расширяя его аргументы.
Аналогично unquote/1.
Примеры
iex> values = [2, 3, 4]
iex> quote do
...> sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]} with(args)Source
Комбинирует сопоставляющие правила.
Один из способов понять with – показать, какие шаблоны кода он улучшает. Представьте себе карту, где поля width и height являются необязательными, и вы хотите вычислить её площадь, как {:ok, area} или вернуть :error. Мы могли бы реализовать эту функцию как:
def area(map) do
case Map.fetch(opts, :width) do
{:ok, width} ->
case Map.fetch(opts, :height) do
{:ok, height} -> {:ok, width * height}
:error -> :error
end
:error ->
:error
end
end
При вызове как area(%{width: 10, height: 15}), она должна вернуть {:ok, 150}. Если какие-либо поля отсутствуют, она возвращает :error.
Хотя приведенный выше код работает, он довольно громоздкий. Используя with, мы можем переписать его как:
def area(map) do
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height) do
{:ok, width * height}
end
end
Вместо определения вложенных case с правилами, мы используем with вместе с оператором PATTERN <- EXPRESSION для сопоставления выражений справа от него с шаблоном слева. Рассматривайте <- как брата =, за исключением того, что, в то время как = генерирует исключение в случае несовпадения, <- просто прервёт цепочку with и вернёт несопоставленное значение.
Давайте попробуем это в IEx:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{:ok, 150}
Если все правила совпадают, выполняется блок do и возвращается его результат. В противном случае цепочка прерывается, и возвращается несопоставленное значение:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
:error
В шаблонах также можно использовать условия:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"}
Как и в for/1, переменные, привязанные внутри with/1, недоступны за пределами with/1.
В правилах также могут использоваться выражения без <-. Например, вы можете выполнить обычные сопоставления с помощью оператора =:
iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil
Поведение любого выражения в правиле такое же, как если бы оно было написано за пределами with. Например, = вызовет исключение MatchError, а не вернёт несопоставленное значение:
with :foo = :bar, do: :ok ** (MatchError) no match of right hand side value: :bar
Как и при любом другом вызове функции или макроса в Elixir, явные скобки также могут быть использованы вокруг аргументов перед блоком do-end:
iex> opts = %{width: 10, height: 15}
iex> with(
...> {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...> {:ok, width * height}
...> end
{:ok, 150}
Выбор между скобками и их отсутствием зависит от предпочтений.
Правила else
Можно указать опцию else для изменения того, что возвращает with в случае неудачного сопоставления:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
Блок else работает как правило case: он может иметь несколько правил, и будет использоваться первое совпадение. Переменные, привязанные внутри with (например, width в этом примере), недоступны в блоке else.
Если используется блок else и нет совпадений с правилами, генерируется исключение WithClauseError.
Внимание!
Следует помнить, что одним из потенциальных недостатков with является то, что все правила обработки ошибок объединяются в один блок else. Например, рассмотрим этот код, который проверяет, указывает ли данный путь на файл Elixir и существует ли он, прежде чем создать резервную копию:
with ".ex" <- Path.extname(path),
true <- File.exists?(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
else
binary when is_binary(binary) ->
{:error, :invalid_extension}
false ->
{:error, :missing_file}
end
Обратите внимание, как нам приходится восстанавливать типы результатов Path.extname/1 и File.exists?/1 для создания сообщений об ошибках. В этом случае лучше переписать код, чтобы каждый <- уже возвращал желаемый формат в случае ошибок, как это:
with :ok <- validate_extension(path),
:ok <- validate_exists(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
end
defp validate_extension(path) do
if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end
defp validate_exists(path) do
if File.exists?(path), do: :ok, else: {:error, :missing_file}
end
Обратите внимание, как приведенный выше код лучше организован и понятнее, если мы убедимся, что каждый <- в with возвращает нормализованный формат.
^varSource
Оператор привязки. Доступ к уже привязанной переменной в правилах сопоставления.
Примеры
Elixir позволяет переопределять переменные через статическое однократное присваивание:
iex> x = 1 iex> x = x + 1 iex> x 2
Однако в некоторых ситуациях полезно сопоставлять существующее значение, а не переопределять его. Это можно сделать с помощью специальной формы ^, разговорно известной как оператор привязки:
iex> x = 1 iex> ^x = List.first([1]) iex> ^x = List.first([2]) ** (MatchError) no match of right hand side value: 2
Обратите внимание, что ^x всегда ссылается на значение x до сопоставления. Следующий пример будет совпадать:
iex> x = 0
iex> {x, ^x} = {1, 0}
iex> x
1 {args}Source
Создаёт кортеж.
Дополнительную информацию о типе данных кортеж и о функциях для работы с кортежами можно найти в модуле Tuple; некоторые функции для работы с кортежами также доступны в Kernel (например, Kernel.elem/2 или Kernel.tuple_size/1).
Представление AST
Только кортежи из двух элементов считаются литералами в Elixir и возвращают себя при цитировании. Поэтому все другие кортежи представлены в AST как вызовы специальной формы :{}.
iex> quote do
...> {1, 2}
...> end
{1, 2}
iex> quote do
...> {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}
© 2012 Plataformatec
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.15.4/Kernel.SpecialForms.html