Kernel.SpecialForms
Специальные формы являются основными строительными блоками Elixir и поэтому не могут быть переопределены разработчиком.
Мы определяем их в этом модуле. Некоторые из этих форм являются лексическими (например, alias/2, case/2 и т. д.). Макросы {}/1 и <<>>/1 также являются специальными формами, используемыми для определения кортежей и двоичных структур данных соответственно.
Этот модуль также документирует макросы, которые возвращают информацию об окружении компиляции Elixir, например (__ENV__/0, __MODULE__/0, __DIR__/0 и __CALLER__/0).
Наконец, он также документирует две специальные формы, __block__/1 и __aliases__/1, которые не предназначены для прямого вызова разработчиком, но они появляются в цитируемом содержимом, поскольку они являются необходимыми элементами конструкций Elixir.
Краткое описание
Функции
- %struct{}
Соответствие или построение структуры.
- %{}
Создает карту.
- &(expr)
Захватывает или создаёт анонимную функцию.
- left . right
Определяет удалённый вызов, вызов анонимной функции или псевдоним.
- left :: right
Используется типами и битовыми строками для указания типов.
- <<args>>
Определяет новую битовую строку.
- left = right
Сопоставляет значение справа с шаблоном слева.
- ^var
Доступ к уже связанной переменной в разделах сопоставления. Также известен как оператор фиксации.
- __CALLER__
Возвращает текущую вызывающую среду как структуру
Macro.Env.- __DIR__
Возвращает абсолютный путь к каталогу текущего файла в виде бинарного значения.
- __MODULE__
Возвращает имя текущего модуля как атом или
nilв противном случае.- __STACKTRACE__
Возвращает трассировку стека для текущего обрабатываемого исключения.
- __aliases__(args)
Внутренняя специальная форма для хранения информации о псевдонимах.
- __block__(args)
Внутренняя специальная форма для блочных выражений.
- alias(module, opts)
alias/2используется для настройки псевдонимов, что часто бывает полезно для имён модулей.- case(condition, clauses)
Сопоставляет данное выражение с заданными разделами.
- cond(clauses)
Вычисляет выражение, соответствующее первому разделу, который вычисляется как истинное значение.
- fn
Определяет анонимную функцию.
- 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)
Используется для объединения разделов сопоставления.
- {args}
Создаёт кортеж.
Функции
%struct{}
Соответствует или создает структуру.
Структура — это помеченное отображение, которое позволяет разработчикам предоставлять значения по умолчанию для ключей, метки для использования в полиморфных диспатчах и утверждениях времени компиляции.
Структуры обычно определяются с помощью макроса 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 %{}
Создает отображение.
См. модуль Map для получения дополнительной информации об отображениях, их синтаксисе и способах доступа к ним и манипулирования ими.
Представление AST
Независимо от того, используется ли => или синтаксис ключевого слова, пары «ключ-значение» в отображениях всегда представляются внутри в виде списка кортежей из двух элементов для простоты:
iex> quote do
...> %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]} &(expr)
Захватывает или создает анонимную функцию.
Захват
Оператор захвата чаще всего используется для захвата функции с заданным именем и арностью из модуля:
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 . right
Определяет удаленный вызов, вызов анонимной функции или псевдоним.
Точка (.) в 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]} left :: right
Используется типами и битовыми строками для указания типов.
Этот оператор используется в двух разных случаях в Elixir. Он используется в описаниях типов для указания типа переменной, функции или самого типа:
@type number :: integer | float @spec add(number, number) :: number
Он также может использоваться в битовых строках для указания типа заданного битового сегмента:
<<int::integer-little, rest::bits>> = bits
См. документацию на странице Typespec и <<>>/1 для получения дополнительной информации о типах и битовых строках соответственно.
<<args>>
Определяет новую битовую строку.
Примеры
iex> <<1, 2, 3>> <<1, 2, 3>>
Типы
Битовая строка состоит из нескольких сегментов, и каждый сегмент имеет тип. В битовых строках используются 9 типов:
-
integer -
float -
bits(псевдоним дляbitstring) -
bitstring -
binary -
bytes(псевдоним дляbinary) -
utf8 -
utf16 -
utf32
Если тип не указан, используется значение по умолчанию integer:
iex> <<1, 2, 3>> <<1, 2, 3>>
По умолчанию Elixir также принимает сегмент в виде литеральной строки или литерального списка символов, которые по умолчанию расширяются до целых чисел:
iex> <<0, "foo">> <<0, 102, 111, 111>>
Переменные или другие типы необходимо явно пометить:
iex> rest = "oo" iex> <<102, rest>> ** (ArgumentError) argument error
Это можно решить, явно пометив его как 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>>
Параметры
Многие параметры можно передать, используя - в качестве разделителя. Порядок произвольный, поэтому следующие варианты эквивалентны:
<<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 должно быть равно 32 или 64, что соответствует стандартам IEEE 754 binary32 и binary64 соответственно.
Для двоичных данных по умолчанию используется размер двоичных данных. Только последний двоичный элемент в совпадении может использовать размер по умолчанию. Все остальные должны иметь явно указанный размер, даже если совпадение однозначно. Например:
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"} Размер может быть переменной:
iex> name_size = 5
iex> <<name::binary-size(name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"} И переменная может быть определена в самом совпадении (до её использования):
iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"} Однако размер нельзя определить в совпадении вне соответствия двоичных/битовых строк:
{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment Отсутствие явного указания размера для непоследнего элемента приводит к ошибке компиляции:
<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">> ** (CompileError): a binary field without size is only allowed at the end of a binary pattern
Сокращенная синтаксическая запись
Размер и единицы также можно указать с помощью сокращенной синтаксической записи при передаче целочисленных значений:
iex> x = 1 iex> <<x::8>> == <<x::size(8)>> true iex> <<x::8*4>> == <<x::size(8)-unit(4)>> true
Этот синтаксис отражает тот факт, что эффективный размер получается умножением размера на единицу.
Модификаторы
Некоторые типы имеют связанные модификаторы, чтобы устранить неоднозначность в представлении байтов.
| Модификатор | Соответствующие типы |
|---|---|
signed | integer |
unsigned (по умолчанию) | integer |
little |
integer, float, utf16, utf32
|
big (по умолчанию) |
integer, float, utf16, utf32
|
native |
integer, utf16, utf32
|
Знак
Целые числа могут быть со знаком signed или без знака unsigned, по умолчанию unsigned.
iex> <<int::integer>> = <<-100>> <<156>> iex> int 156 iex> <<int::integer-signed>> = <<-100>> <<156>> iex> int -100
signed и unsigned используются только для сопоставления двоичных данных (см. ниже) и только для целых чисел.
iex> <<-100::signed, _rest::binary>> = <<-100, "foo">> <<156, 102, 111, 111>>
Порядок байтов
В Elixir есть три варианта порядка байтов: big, little, и native. По умолчанию big:
iex> <<number::little-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 256 iex> <<number::big-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 1
native определяется виртуальной машиной при запуске и зависит от операционной системы хоста.
Сопоставление двоичных/битовых строк
Сопоставление двоичных данных — мощная функция в Elixir, полезная для извлечения информации из двоичных данных и шаблоносопоставления.
Сопоставление двоичных данных может использоваться само по себе для извлечения информации из двоичных данных:
iex> <<"Hello, ", place::binary>> = "Hello, World" "Hello, World" iex> place "World"
Или как часть определений функций для шаблоносопоставления:
defmodule ImageTyper do
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, rest::binary>>), do: :png
def type(<<@jpg_signature, rest::binary>>), do: :jpg
def type(_), do: :unknown
end Производительность и оптимизации
Компилятор Erlang может предоставить ряд оптимизаций при создании и сопоставлении двоичных данных. Чтобы увидеть вывод оптимизации, установите параметр компиляции bin_opt_info:
ERL_COMPILER_OPTIONS=bin_opt_info mix compile
Дополнительную информацию об отдельных оптимизациях и соображениях по производительности можно найти в Руководстве Erlang по эффективной обработке двоичных данных.
left = right
Сопоставляет значение справа с шаблоном слева.
^var
Получает уже связанную переменную в правилах сопоставления. Также известен как оператор привязки.
Примеры
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 __CALLER__
Возвращает текущую среду вызова в виде структуры Macro.Env.
В среде можно получить имя файла, номера строк, настроить псевдонимы, функцию и другие данные.
__DIR__
Возвращает абсолютный путь к каталогу текущего файла в виде двоичных данных.
Хотя к каталогу можно получить доступ как Path.dirname(__ENV__.file), эта макрокоманда является удобным сокращением.
__ENV__
Возвращает текущую информацию о среде в виде структуры Macro.Env.
В среде можно получить текущее имя файла, номера строк, настроить псевдонимы, текущую функцию и другие данные.
__MODULE__
Возвращает имя текущего модуля как атом или nil в противном случае.
Хотя к модулю можно получить доступ в __ENV__/0, эта макрокоманда является удобным сокращением.
__STACKTRACE__
Возвращает стек вызовов для текущей обработанной исключительной ситуации.
Доступно только в правилах catch и rescue выражений try/1.
Чтобы получить стек вызовов текущего процесса, используйте Process.info(self(), :current_stacktrace).
__aliases__(args)
Внутренняя специальная форма для хранения информации о псевдонимах.
Обычно компилируется в атом:
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)
Внутренняя специальная форма для блочных выражений.
Используется, когда есть блок выражений в Elixir. Эта специальная форма является закрытой и не должна вызываться напрямую:
iex> quote do
...> 1
...> 2
...> 3
...> end
{:__block__, [], [1, 2, 3]} alias(module, opts)
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)
Сопоставляет данное выражение с заданными условиями.
Примеры
case thing do
{:selector, i, value} when is_integer(i) ->
value
value ->
value
end В примере выше мы сопоставляем thing с каждой частью "заголовка" и выполняем часть "тела", соответствующую первому совпавшему условию.
Если ни одно условие не совпадает, генерируется ошибка. Поэтому может потребоваться добавить заключительное условие (например, _), которое всегда будет совпадать.
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 or 13
В примере выше, value будет 7 или 13 в зависимости от значения lucky?. Если у value нет предыдущего значения до case, условия, которые не явно привязывают значение, привязывают переменную к nil.
Если вы хотите сопоставить шаблон с существующей переменной, вам необходимо использовать оператор ^/1:
x = 1 case 10 do ^x -> "Won't match" _ -> "Will match" end #=> "Will match"
cond(clauses)
Вычисляет выражение, соответствующее первому условию, которое вычисляется как истинное.
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
Определяет анонимную функцию.
Примеры
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)
Понимания позволяют быстро создавать структуру данных из перечислимого объекта или битового массива.
Начнём с примера:
iex> for n <- [1, 2, 3, 4], do: n * 2 [2, 4, 6, 8]
Понимание принимает множество генераторов и фильтров. Перечисляемые генераторы определяются с помощью <-:
# A list generator: iex> for n <- [1, 2, 3, 4], do: n * 2 [2, 4, 6, 8] # A comprehension with two generators iex> for x <- [1, 2], y <- [2, 3], do: x * y [2, 3, 4, 6]
Также можно задать фильтры:
# A comprehension with a generator and a filter iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n [2, 4, 6]
Генераторы также могут использоваться для фильтрации, так как удаляются все значения, которые не соответствуют шаблону слева от <-:
iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...> String.upcase(name)
...> end
["JOHN", "MEG"] Также поддерживаются генераторы битовых массивов, которые очень полезны, когда необходимо организовать потоки битовых массивов:
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}] Присваивания переменных внутри понимания, будь то в генераторах, фильтрах или внутри блока, не отражаются за пределами понимания.
Опции :into и :uniq
В примерах выше возвращаемым результатом понимания всегда был список. Возвращаемый результат можно настроить, передав опцию :into, которая принимает любую структуру, если она реализует протокол Collectable.
Например, мы можем использовать генераторы битовых массивов с опцией :into для легкого удаления всех пробелов в строке:
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>> "helloworld"
Модуль IO предоставляет потоки, которые являются как Enumerable, так и Collectable. Вот пример сервера эхо с преобразованием в верхний регистр, использующий понимание:
for line <- IO.stream(:stdio, :line), into: IO.stream(:stdio, :line) 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)
Импортирует функции и макросы из других модулей.
import/2 позволяет легко получить доступ к функциям или макросам из других модулей без использования квалифицированного имени.
Примеры
Если вы используете несколько функций из данного модуля, вы можете импортировать эти функции и ссылаться на них как на локальные функции, например:
iex> import List iex> flatten([1, [2], 3]) [1, 2, 3]
Выбор
По умолчанию Elixir импортирует функции и макросы из данного модуля, за исключением тех, которые начинаются с символа подчеркивания (обычно это колбэки):
import List
Разработчик может отфильтровать импорт, чтобы импортировать только макросы или функции, используя опцию only:
import List, only: :functions import List, only: :macros
В качестве альтернативы, Elixir позволяет разработчику передавать пары имя/арность в :only или :except для более тонкого управления тем, что импортировать (или не импортировать):
import List, only: [flatten: 1] import String, except: [split: 2]
Обратите внимание, что вызов except всегда исключителен по отношению к ранее объявленному import/2. Если предыдущего импорта нет, то он применяется ко всем функциям и макросам в модуле. Например:
import List, only: [flatten: 1, keyfind: 4] import List, except: [flatten: 1]
После двух вызовов импорта выше, будет импортирована только List.keyfind/4.
Функции с подчеркиванием
По умолчанию функции, начинающиеся с _ не импортируются. Если вам действительно нужно импортировать функцию, начинающуюся с _, вы должны явно включить её в селектор :only.
import File.Stream, only: [__build__: 3]
Лексическая область видимости
Важно отметить, что import/2 лексический. Это означает, что вы можете импортировать определенные макросы внутри определенных функций:
defmodule Math do
def some_function do
# 1) Disable "if/2" from Kernel
import Kernel, except: [if: 2]
# 2) Require the new "if/2" macro from MyMacros
import MyMacros
# 3) Use the new macro
if do_something, it_works
end
end В приведенном примере мы импортировали макросы из MyMacros, заменяя исходную реализацию if/2 на собственную внутри этой конкретной функции. Все остальные функции в этом модуле по-прежнему смогут использовать оригинальную.
Предупреждения
Если вы импортируете модуль и не используете ни одну из импортированных функций или макросов из этого модуля, Elixir выведет предупреждение, предполагая, что импорт не используется.
В случае, если импорт генерируется автоматически макросом, Elixir не будет выводить никаких предупреждений, так как импорт не был явно определен.
Оба поведения предупреждений можно изменить, явно установив опцию :warn на true или false.
Неоднозначные имена функций/макросов
Если два модуля A и B импортированы и оба содержат функцию foo с арностью 1, ошибка выдается только если фактически сделан неоднозначный вызов foo/1; то есть ошибки выдаются лениво, а не жадно.
quote(opts, block)
Получает представление любого выражения.
Примеры
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.
Параметры
-
:unquote— когдаfalse, отключает расквывание. Это означает, что любой вызовunquoteбудет сохранён как есть в AST, а не заменён аргументамиunquote. Например:iex> quote do ...> unquote("hello") ...> end "hello" iex> quote unquote: false do ...> unquote("hello") ...> end {:unquote, [], ["hello"]} -
:location— при значении:keep, сохраняет текущую строку и файл из quote. Подробную информацию см. в разделе Информация об отслеживании стека ниже. -
:line— устанавливает для цитируемых выражений указанную строку. -
:generated— отмечает указанный фрагмент как сгенерированный, чтобы не генерировать предупреждения. В настоящее время это работает только со специальными формами (например, вы можете аннотироватьcase, но неif). -
:context— устанавливает контекст разрешения. -
:bind_quoted— передаёт связывание макросу. В случае передачи связывания,unquote/1автоматически отключается.
Quote и макросы
quote/2 обычно используется с макросами для генерации кода. В качестве упражнения давайте определим макрос, который умножает число на себя (возведение в квадрат). На практике нет необходимости определять такой макрос (и на самом деле это считается плохой практикой), но он достаточно прост, чтобы сосредоточиться на важных аспектах quote и макросов:
defmodule Math do
defmacro squared(x) do
quote do
unquote(x) * unquote(x)
end
end
end Его можно вызвать так:
import Math
IO.puts("Got #{squared(5)}") Сначала в этом примере нет ничего, что бы показало, что это макрос. Но на стадии компиляции squared(5) превращается в 5 * 5. Аргумент 5 дублируется в сгенерированном коде, и мы можем наблюдать это на практике, потому что в нашем макросе есть ошибка:
import Math
my_number = fn ->
IO.puts("Returning 5")
5
end
IO.puts("Got #{squared(my_number.())}") Приведённый выше пример выведет:
Returning 5 Returning 5 Got 25
Обратите внимание, что «Возвращает 5» выведено дважды, а не один раз. Это происходит потому, что макрос получает выражение, а не значение (что ожидается от обычной функции). Это означает, что:
squared(my_number.())
В действительности расширяется до:
my_number.() * my_number.()
Что вызывает функцию дважды, объясняя, почему выводимое значение выводится дважды! В большинстве случаев это нежелаемое поведение, и поэтому важно помнить, что при работе с макросами нельзя расквывать одно и то же значение более одного раза.
Давайте исправим наш макрос:
defmodule Math do
defmacro squared(x) do
quote do
x = unquote(x)
x * x
end
end
end Теперь вызов squared(my_number.()) как и раньше выведет значение только один раз.
На самом деле, этот шаблон настолько распространён, что в большинстве случаев вы захотите использовать параметр bind_quoted с quote/2:
defmodule Math do
defmacro squared(x) do
quote bind_quoted: [x: x] do
x * x
end
end
end :bind_quoted будет транслироваться в такой же код, как пример выше. :bind_quoted может быть использован во многих случаях и считается хорошей практикой, не только потому, что помогает избежать распространённых ошибок, но и потому, что позволяет использовать другие инструменты, предоставляемые макросами, такие как фрагменты расквывания, обсуждаемые в некоторых разделах ниже.
Прежде чем закончить это краткое введение, вы заметите, что, хотя мы определили переменную x в нашем quote:
quote do x = unquote(x) x * x end
Когда мы вызываем:
import Math squared(5) x #=> ** (CompileError) undefined variable x or undefined function x/0
Мы можем видеть, что x не попала в контекст пользователя. Это происходит потому, что макросы Elixir являются гигиеничными, тема, которую мы подробно обсудим в следующих разделах.
Гигиена переменных
Рассмотрим следующий пример:
defmodule Hygiene do
defmacro no_interference do
quote do
a = 1
end
end
end
require Hygiene
a = 10
Hygiene.no_interference()
a
#=> 10 В приведённом выше примере a возвращает 10, даже если макрос, по-видимому, устанавливает его в 1, потому что переменные, определённые в макросе, не влияют на контекст, в котором выполняется макрос. Если вы хотите установить или получить переменную в контексте вызывающей функции, вы можете сделать это с помощью макроса var!:
defmodule NoHygiene do
defmacro interference do
quote do
var!(a) = 1
end
end
end
require NoHygiene
a = 10
NoHygiene.interference()
a
#=> 1 Вы не можете даже получить доступ к переменным, определённым в том же модуле, если не укажете явно контекст:
defmodule Hygiene do
defmacro write do
quote do
a = 1
end
end
defmacro read do
quote do
a
end
end
end
Hygiene.write()
Hygiene.read()
#=> ** (RuntimeError) undefined variable a or undefined function a/0 Для этого вы можете явно передать текущий контекст модуля как аргумент:
defmodule ContextHygiene do
defmacro write do
quote do
var!(a, ContextHygiene) = 1
end
end
defmacro read do
quote do
var!(a, ContextHygiene)
end
end
end
ContextHygiene.write()
ContextHygiene.read()
#=> 1 Гигиена псевдонимов
Псевдонимы внутри quote по умолчанию гигиеничны. Рассмотрим следующий пример:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
Hygiene.no_interference()
#=> %{} Обратите внимание, что, даже если псевдоним M недоступен в контексте расширения макроса, код выше работает, потому что M всё ещё расширяется до Map.
Аналогично, даже если мы определили псевдоним с таким же именем перед вызовом макроса, он не повлияет на результат макроса:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{} В некоторых случаях вы хотите получить доступ к псевдониму или модулю, определённому в вызывающей функции. Для этого вы можете использовать макрос alias!:
defmodule Hygiene do
# This will expand to Elixir.Nested.hello()
defmacro no_interference do
quote do
Nested.hello()
end
end
# This will expand to Nested.hello() for
# whatever is Nested in the caller
defmacro interference do
quote do
alias!(Nested).hello()
end
end
end
defmodule Parent do
defmodule Nested do
def hello, do: "world"
end
require Hygiene
Hygiene.no_interference()
#=> ** (UndefinedFunctionError) ...
Hygiene.interference()
#=> "world"
end Гигиена импортов
Аналогично псевдонимам, импорты в Elixir гигиеничны. Рассмотрим следующий код:
defmodule Hygiene do
defmacrop get_length do
quote do
length([1, 2, 3])
end
end
def return_length do
import Kernel, except: [length: 1]
get_length
end
end
Hygiene.return_length()
#=> 3 Обратите внимание, как Hygiene.return_length/0 возвращает 3, даже если функция Kernel.length/1 не импортирована. На самом деле, даже если return_length/0 импортировал функцию с тем же именем и арностью из другого модуля, это не повлияет на результат функции:
def return_length do import String, only: [length: 1] get_length end
Вызов этого нового return_length/0 по-прежнему вернёт 3 в качестве результата.
Elixir достаточно умен, чтобы отложить разрешение до последнего возможного момента. Таким образом, если вы вызываете length([1, 2, 3]) внутри quote, но функция length/1 недоступна, она расширяется в вызывающей функции:
defmodule Lazy do
defmacrop get_length do
import Kernel, except: [length: 1]
quote do
length("hello")
end
end
def return_length do
import Kernel, except: [length: 1]
import String, only: [length: 1]
get_length
end
end
Lazy.return_length()
#=> 5 Информация об отслеживании стека
При определении функций с помощью макросов разработчики могут выбрать, будут ли ошибки времени выполнения сообщаться из вызывающей функции или изнутри quote. Посмотрим на пример:
# adder.ex
defmodule Adder do
@doc "Defines a function that adds two numbers"
defmacro defadd do
quote location: :keep do
def add(a, b), do: a + b
end
end
end
# sample.ex
defmodule Sample do
import Adder
defadd
end
require Sample
Sample.add(:one, :two)
#=> ** (ArithmeticError) bad argument in arithmetic expression
#=> adder.ex:5: Sample.add/2 Когда используется location: :keep и Sample.add/2 получает недопустимые аргументы, информация об отслеживании стека будет указывать на файл и строку внутри quote. Без location: :keep, ошибка сообщается в том месте, где был вызван defadd. location: :keep влияет только на определения внутри quote.
Связывание и фрагменты расквывания
Механизмы quote/unquote 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 рекомендуется каждый раз, когда необходимо ввести значение в quote.
receive(args)
Проверяет, есть ли сообщение, соответствующее заданным условиям, в почтовом ящике текущего процесса.
В случае отсутствия такого сообщения текущий процесс ожидает, пока придёт сообщение или пока не истечёт заданное время ожидания.
Примеры
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)
Загружает модуль для использования его макросов.
Примеры
Общедоступные функции в модулях глобально доступны, но для использования макросов необходимо явно загрузить модуль, в котором они определены.
Предположим, вы реализовали свою собственную версию if/2 в модуле MyMacros. Если вы хотите вызвать её, вам необходимо сначала явно загрузить модуль MyMacros.
defmodule Math do require MyMacros MyMacros.if do_something, it_works end
Попытка вызвать макрос, который не был загружен, вызовет ошибку.
Сокращение для псевдонимов
require/2 также принимает :as в качестве параметра, чтобы автоматически создать псевдоним. Подробнее см. в alias/2.
super(args)
Вызывает переопределённую функцию при переопределении её с помощью Kernel.defoverridable/1.
См. Kernel.defoverridable/1 для получения дополнительной информации и документации.
try(args)
Вычисляет заданные выражения и обрабатывает любые ошибки, выходы или броски, которые могли произойти.
Примеры
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 a single exception without binding the exception # to a variable try do UndefinedModule.undefined_function rescue UndefinedFunctionError -> nil end # Rescue any of the given exception without binding try do UndefinedModule.undefined_function rescue [UndefinedFunctionError, ArgumentError] -> nil end # Rescue and bind the exception to the variable "x" try do UndefinedModule.undefined_function rescue x in [UndefinedFunctionError] -> nil end # Rescue all kinds of exceptions and bind the rescued exception # to the variable "x" try do UndefinedModule.undefined_function rescue x -> nil end
Ошибки Erlang
Ошибки Erlang преобразуются в Elixir ошибки при перехвате:
try do :erlang.error(:badarg) rescue ArgumentError -> :ok end #=> :ok
Наиболее распространённые ошибки Erlang преобразуются в их эквиваленты в Elixir. Те, которые не преобразуются, преобразуются в более общую ErlangError:
try do :erlang.error(:unknown) rescue ErlangError -> :ok end #=> :ok
Фактически, ErlangError может быть использована для перехвата любых ошибок, которые не являются надлежащими ошибками Elixir. Например, она может быть использована для перехвата ранее упомянутой ошибки :badarg до преобразования:
try do :erlang.error(:badarg) rescue ErlangError -> :ok end #=> :ok
Условия catch
Условие catch может быть использовано для перехвата брошенных значений, выходов и ошибок.
Перехват брошенных значений
catch может быть использовано для перехвата значений, брошенных с помощью Kernel.throw/1:
try do
throw(:some_value)
catch
thrown_value ->
IO.puts("A value was thrown: #{inspect(thrown_value)}")
end Перехват значений любого типа
Условие catch также поддерживает перехват выходов и ошибок. Для этого оно позволяет сопоставлять как тип перехваченного значения, так и само значение:
try do
exit(:shutdown)
catch
:exit, value ->
IO.puts("Exited with value #{inspect(value)}")
end
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts("Caught exit or throw with value #{inspect(value)}")
end Условие catch также поддерживает :error наряду с :exit и :throw, как в Erlang, хотя это обычно избегается в пользу механизмов управления raise/rescue. Одна из причин этого заключается в том, что при перехвате :error, ошибка не преобразуется автоматически в ошибку Elixir:
try do :erlang.error(:badarg) catch :error, :badarg -> :ok end #=> :ok
Условия after
Условие after позволяет определять логику очистки, которая будет вызвана как при успешном выполнении блока кода, переданного try/1, так и при возникновении ошибки. Обратите внимание, что процесс завершит свою работу как обычно при получении сигнала выхода, вызывающего его внезапное завершение, поэтому условие after не гарантируется для выполнения. К счастью, большинство ресурсов в Elixir (такие как открытые файлы, таблицы ETS, порты, сокеты и так далее) связаны с собственным процессом или отслеживают его и автоматически очищаются, если этот процесс завершается.
File.write!("tmp/story.txt", "Hello, World")
try do
do_something_with("tmp/story.txt")
after
File.rm("tmp/story.txt")
end Условия else
Условия else позволяют сопоставить результат тела, переданного try/1:
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end Если условие else отсутствует и исключения не возникают, возвращается результат выражения:
x = 1
^x =
try do
1 / x
rescue
ArithmeticError ->
:infinity
end Однако, если условие else присутствует, но результат выражения не соответствует ни одному из шаблонов, будет поднято исключение. Это исключение не будет перехвачено условием catch или rescue в том же try:
x = 1
try do
try do
1 / x
rescue
# The TryClauseError cannot be rescued here:
TryClauseError ->
:error_a
else
0 ->
:small
end
rescue
# The TryClauseError is rescued here:
TryClauseError ->
:error_b
end Аналогично, исключение внутри условия else не перехватывается и не обрабатывается внутри того же try:
try do
try do
nil
catch
# The exit(1) call below can not be caught here:
:exit, _ ->
:exit_a
else
_ ->
exit(1)
end
catch
# The exit is caught here:
:exit, _ ->
:exit_b
end Это означает, что виртуальная машина больше не нуждается в сохранении стека отслеживания после попадания в условие else, и поэтому хвостовая рекурсия возможна при использовании try с хвостовым вызовом как конечным вызовом внутри условия else. То же самое верно для условий rescue и catch.
Только результат вычисленного выражения передаётся в условие else Если условие try попадает в условия rescue или catch, их результат не будет передан в else:
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end Обработка переменных
Поскольку выражение внутри try может не быть вычислено из-за исключения, любая переменная, созданная внутри try не может быть обращена снаружи. Например:
try do x = 1 do_something_that_may_fail(same_arg) :ok catch _, _ -> :failed end x #=> unbound variable "x"
В приведённом примере переменную x нельзя получить, так как она была определена внутри условия try. Распространённым способом решения этой проблемы является возврат переменных, определённых внутри try:
x =
try do
x = 1
do_something_that_may_fail(same_arg)
x
catch
_, _ -> :failed
end unquote(expr)
Раскусывает заданное выражение внутри цитированного выражения.
Эта функция ожидает допустимое выражение AST Elixir, также известное как цитированное выражение, в качестве аргумента. Если вы хотите unquote любое значение, такое как карта или четырёхэлементная кортеж, вы должны вызвать Macro.escape/1 перед расключением.
Примеры
Представьте себе ситуацию, когда у вас есть цитированное выражение, и вы хотите вставить его внутрь другой цитаты. Первая попытка будет:
value =
quote do
13
end
quote do
sum(1, value, 3)
end Что затем вернёт:
{:sum, [], [1, {:value, [], Elixir}, 3]} Что не является ожидаемым результатом. Для этого мы используем unquote:
iex> value =
...> quote do
...> 13
...> end
iex> quote do
...> sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]} Если вы хотите расклеить значение, которое не является цитированным выражением, например карту, вам нужно вызвать Macro.escape/1 перед этим:
iex> value = %{foo: :bar}
iex> quote do
...> process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]} Если вы забудете это сделать, Elixir выдаст ошибку при компиляции кода.
unquote_splicing(expr)
Раскусывает заданный список, расширяя его аргументы.
Аналогично 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)
Используется для объединения сопоставляющих условий.
Начнем с примера:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{:ok, 150} Если все условия сопоставляются, выполняется блок do и возвращается его результат. В противном случае цепочка прерывается, и возвращается несопоставленное значение:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
:error Ограничения могут быть использованы и в шаблонах:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"} Как и в for/1, переменные, привязанные внутри with/1, не будут «утечь». Выражения без <- также могут быть использованы в условиях. Например, вы можете выполнять обычные сопоставления с оператором =:
iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil Поведение любого выражения в условии такое же, как и вне его. Например, = вызовет 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 может быть задана для изменения того, что возвращается из 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}
...> end
{:error, :wrong_data} Если используется блок else и нет совпадающих условий, генерируется исключение WithClauseError.
{args}
Создает кортеж.
Дополнительную информацию о типе данных кортежа и функциях для работы с кортежами можно найти в модуле 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.9.4/Kernel.SpecialForms.html