Исходный код Kernel.SpecialForms
Специальные формы являются основными строительными блоками Elixir и поэтому не могут быть переопределены разработчиком.
Модуль Kernel.SpecialForms состоит только из макросов, которые могут быть вызваны в любом месте кода Elixir без использования префикса Kernel.SpecialForms.. Это возможно, потому что все они были автоматически импортированы, так же как функции и макросы из модуля Kernel.
Эти строительные блоки определены в этом модуле. Некоторые из этих специальных форм являются лексическими (такими как alias/2 и case/2). Макросы {}/1 и <<>>/1 также являются специальными формами, используемыми для определения кортежей и бинарных структур данных соответственно.
Этот модуль также документирует макросы, которые возвращают информацию об окружении компиляции Elixir, такую как (__ENV__/0, __MODULE__/0, __DIR__/0, __STACKTRACE__/0 и __CALLER__/0).
Кроме того, он документирует две специальные формы __block__/1 и __aliases__/1, которые не предназначены для прямого вызова разработчиком, но они появляются в цитируемом содержимом, так как являются необходимыми в конструкциях Elixir.
Краткое описание
Функции
- %struct{}
Сопоставляет или строит структуру.
- %{}
Создает карту.
- &expr
Оператор захвата. Захватывает или создает анонимную функцию.
- left . right
Оператор точки. Определяет удаленный вызов, вызов анонимной функции или псевдоним.
- __aliases__(args)
Внутренняя специальная форма для хранения информации о псевдонимах.
- __block__(args)
Внутренняя специальная форма для блочных выражений.
- __CALLER__
Возвращает текущую среду вызова в виде структуры
Macro.Env.- __cursor__(args)
Внутренняя специальная форма для позиции курсора.
- __DIR__
Возвращает абсолютный путь к каталогу текущего файла в виде бинарного.
- __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
Преимущества структур заключаются в проверке того, что заданные ключи являются частью определенной структуры. Пример ниже завершится ошибкой, так как нет ключа :full_name в структуре User.
%User{full_name: "john doe"}
Также доступна операция обновления, специфичная для структур:
%User{user | age: 28}
Еще раз, приведенный синтаксис гарантирует, что заданные ключи будут валидны на этапе компиляции, и он гарантирует на этапе выполнения, что переданный аргумент является структурой, иначе вызовет ошибку BadStructError. Синтаксис обновления карт также может использоваться для обновления структур, что полезно, когда вы хотите обновить любую структуру, независимо от ее имени, если у них есть соответствующие поля:
%{user | age: 28}
Сопоставление с образцом по именам структур
Помимо возможности сопоставления с образцом по полям структуры, например:
%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
Обратите внимание, что &local_function/1 создает локальный захват, а &__MODULE__.local_function/1 или &imported_function/1 создают удалённый захват. Более подробную информацию см. в разделе «Функции» справочника Erlang по адресу "Функции" в справочнике Erlang.
Локальный или удаленный захват имеет значение при использовании горячей перезагрузки кода: локальные захваты передают вызов версии модуля, существовавшей в момент их создания, в то время как удалённые захваты передают вызов текущей версии модуля.
См. также 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.
В среде можно получить доступ к имени файла, номерам строк, создать псевдонимы, функцию и другое.
__cursor__(args)Source
Внутренняя специальная форма для положения курсора.
Это специальная форма, используемая всякий раз, когда нам нужно представить положение курсора в AST Elixir. См. Code.Fragment для получения дополнительной информации.
__DIR__Source
Возвращает абсолютный путь к директории текущего файла в виде бинарного.
Хотя директория может быть доступна как Path.dirname(__ENV__.file), эта макрокоманда является удобным сокращением.
__ENV__Source
Возвращает текущую информацию об окружении как структуру Macro.Env.
В окружении вы можете получить доступ к текущему имени файла, номерам строк, настроить псевдонимы, текущей функции и другим данным.
__MODULE__Source
Возвращает имя текущего модуля в виде атома или nil в противном случае.
Хотя к модулю можно получить доступ в __ENV__/0, эта макрокоманда является удобным сокращением.
__STACKTRACE__Source
Возвращает стек вызовов для текущего обработанного исключения.
Он доступен только в catch и rescue разделах выражений try/1.
Для получения стека вызовов текущего процесса используйте Process.info(self(), :current_stacktrace).
left :: rightSource
Оператор типа. Используется типами и битовыми строками для указания типов.
Этот оператор используется в Elixir в двух различных случаях. Он используется в типах для указания типа переменной, функции или самого типа:
@type number :: integer | float @spec add(number, number) :: number
Также может использоваться в битовых строках для указания типа данного битового сегмента:
<<int::integer-little, rest::bits>> = bits
Обратитесь к документации на странице Типовые спецификации и <<>>/1 для получения дополнительной информации о типах и битовых строках соответственно.
<<args>>Source
Определяет новую битовую строку.
Примеры
iex> <<1, 2, 3>> <<1, 2, 3>>
Типы
Битовая строка состоит из множества сегментов, и каждый сегмент имеет тип. В битовых строках используется 9 типов:
integerfloat-
bits(псевдоним дляbitstring) bitstringbinary-
bytes(псевдоним дляbinary) utf8utf16utf32
Если тип не указан, используется значение по умолчанию integer:
iex> <<1, 2, 3>> <<1, 2, 3>>
Elixir также по умолчанию принимает сегмент как строку-литерал, которая расширяется до целых чисел:
iex> <<0, "foo">> <<0, 102, 111, 111>>
Вы можете использовать один из utf8 (по умолчанию), utf16, и utf32 для управления кодированием строки:
iex> <<"foo"::utf16>> <<0, 102, 0, 111, 0, 111>>
Что эквивалентно написанию:
iex> <<?f::utf16, ?o::utf16, ?o::utf16>> <<0, 102, 0, 111, 0, 111>>
Во время выполнения бинарные данные должны быть явно помечены как binary:
iex> rest = "oo" iex> <<102, rest::binary>> "foo"
В противном случае мы получим ArgumentError при построении бинарных данных:
rest = "oo" <<102, rest>> ** (ArgumentError) argument error
Параметры
Множество параметров можно указать, используя - в качестве разделителя. Порядок произвольный, поэтому следующие варианты равнозначны:
<<102::integer-native, rest::binary>> <<102::native-integer, rest::binary>> <<102::unsigned-big-integer, rest::binary>> <<102::unsigned-big-integer-size(8), rest::binary>> <<102::unsigned-big-integer-8, rest::binary>> <<102::8-integer-big-unsigned, rest::binary>> <<102, rest::binary>>
Единица и Размер
Длина совпадения равна unit (число битов) умноженному на size (количество повторяющихся сегментов длиной unit).
| Тип | Единица по умолчанию |
|---|---|
integer |
1 бит |
float |
1 бит |
binary |
8 бит |
Размеры для типов немного сложнее. Размер целых чисел по умолчанию равен 8.
Для чисел с плавающей запятой он равен 64. Для чисел с плавающей запятой size * unit должно быть равно 16, 32 или 64, что соответствует IEEE 754 binary16, binary32 и binary64 соответственно.
Для бинарных данных по умолчанию используется размер бинарных данных. Только последняя бинарная строка в совпадении может использовать размер по умолчанию. Все остальные должны иметь свой размер, указанный явно, даже если совпадение однозначное. Например:
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}
Размер может быть переменной или любым допустимым выражением защиты:
iex> name_size = 5
iex> <<name::binary-size(^name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
Размер может получить доступ к предыдущим переменным, определённым в самой бинарной строке:
iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
Однако он не может получить доступ к переменным, определённым в совпадении вне бинарных/битовых строк:
{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment
Если размер не указан для не последнего сегмента, компиляция завершится ошибкой:
<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">> ** (CompileError): a binary field without size is only allowed at the end of a binary pattern
Сокращенная запись
Размер и единицы также могут быть указаны с помощью сокращенной записи при передаче целочисленных значений:
iex> x = 1 iex> <<x::8>> == <<x::size(8)>> true iex> <<x::8*4>> == <<x::size(8)-unit(4)>> true
Эта запись отражает тот факт, что эффективный размер получается путём умножения размера на единицу.
Модификаторы
У некоторых типов есть связанные модификаторы, чтобы устранить неоднозначность в представлении байтов.
| Модификатор | Соответствующий тип(ы) |
|---|---|
signed |
integer |
unsigned (по умолчанию) |
integer |
little |
integer, float, utf16, utf32
|
big (по умолчанию) |
integer, float, utf16, utf32
|
native |
integer, float, utf16, utf32
|
Знак
Целые числа могут быть signed или unsigned, по умолчанию unsigned.
iex> <<int::integer>> = <<-100>> <<156>> iex> int 156 iex> <<int::integer-signed>> = <<-100>> <<156>> iex> int -100
signed и unsigned используются только для соответствия бинарным данным (см. ниже) и только для целых чисел.
iex> <<-100::signed, _rest::binary>> = <<-100, "foo">> <<156, 102, 111, 111>>
Порядок байтов
В Elixir есть три варианта порядка байтов: big, little, и native. По умолчанию big:
iex> <<number::little-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 256 iex> <<number::big-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 1
native определяется виртуальной машиной при запуске и будет зависеть от операционной системы хоста.
Сопоставление бинарных/битовых строк
Сопоставление бинарных строк — мощная функция в Elixir, полезная для извлечения информации из бинарных данных и для сопоставления с шаблонами.
Сопоставление бинарных строк может использоваться само по себе для извлечения информации из бинарных данных:
iex> <<"Hello, ", place::binary>> = "Hello, World" "Hello, World" iex> place "World"
Или как часть определений функций для сопоставления с шаблонами:
defmodule ImageType do
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, _rest::binary>>), do: :png
def type(<<@jpg_signature, _rest::binary>>), do: :jpg
def type(_), do: :unknown
end
Производительность и оптимизации
Компилятор Erlang может предоставить ряд оптимизаций для создания и сопоставления бинарных данных. Чтобы увидеть вывод оптимизаций, установите параметр компилятора bin_opt_info:
ERL_COMPILER_OPTIONS=bin_opt_info mix compile
Для получения дополнительной информации о конкретных оптимизациях и соображениях производительности обратитесь к главе «Создание и сопоставление бинарных данных» в руководстве по производительности Erlang.
left = rightSource
Оператор сопоставления. Сопоставляет значение справа с шаблоном слева.
alias(module, opts)Source
alias/2 используется для установки псевдонимов, что часто бывает полезно с именами модулей.
Примеры
alias/2 может использоваться для установки псевдонима для любого модуля:
defmodule Math do alias MyKeyword, as: Keyword end
В примере выше мы установили псевдоним для MyKeyword как Keyword. Теперь любая ссылка на Keyword будет автоматически заменена на MyKeyword.
В случае необходимости доступа к исходному Keyword, можно сделать это, обратившись к Elixir:
Keyword.values #=> uses MyKeyword.values Elixir.Keyword.values #=> uses Keyword.values
Обратите внимание, что вызов alias без опции :as автоматически устанавливает псевдоним на основе последней части модуля. Например:
alias Foo.Bar.Baz
Это то же самое, что:
alias Foo.Bar.Baz, as: Baz
Мы также можем установить псевдонимы для нескольких модулей в одной строке:
alias Foo.{Bar, Baz, Biz}
Это то же самое, что:
alias Foo.Bar alias Foo.Baz alias Foo.Biz
Лексическая область видимости
import/2, require/2 и alias/2 называются директивами и все имеют лексическую область видимости. Это означает, что вы можете устанавливать псевдонимы внутри определенных функций, и это не повлияет на общую область видимости.
Предупреждения
Если вы устанавливаете псевдоним для модуля и не используете его, Elixir выведет предупреждение, предполагая, что псевдоним не используется.
Если псевдоним генерируется автоматически макросом, Elixir не будет выдавать никаких предупреждений, так как псевдоним не был явно определен.
Оба поведения предупреждений можно изменить, явно установив опцию :warn в значение true или false.
case(condition, clauses)Source
Сопоставляет данное выражение с заданными разделами.
case/2 опирается на сопоставление с образцом и условия, чтобы выбрать, какой раздел выполнить. Если ваша логика не может быть выражена в рамках шаблонов и условий, рассмотрите использование if/2 или cond/1.
Примеры
case File.read(file) do
{:ok, contents} when is_binary(contents) ->
String.split(contents, "\n")
{:error, _reason} ->
Logger.warning "could not find #{file}, assuming empty..."
[]
end
В примере выше мы сопоставляем результат File.read/1 с каждым разделом "заголовок" и выполняем раздел "тело", соответствующий первому разделу, который совпадает.
Если ни один раздел не совпадает, генерируется ошибка. По этой причине может потребоваться добавить заключительный раздел (например, _), который всегда будет соответствовать.
x = 10
case x do
0 ->
"This clause won't match"
_ ->
"This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"
Если вы обнаружите, что вкладываете выражения case внутри выражений case , рассмотрите использование with/1.
Обработка переменных
Обратите внимание, что переменные, привязанные в разделе, не "утекают" во внешний контекст:
case data do
{:ok, value} -> value
:error -> nil
end
value
#=> unbound variable value
Переменные во внешнем контексте также не могут быть перезаписаны:
value = 7 case lucky? do false -> value = 13 true -> true end value #=> 7
В приведенном выше примере value будет 7 независимо от значения lucky?. Переменная value , привязанная в разделе, и переменная value , привязанная во внешнем контексте, являются совершенно отдельными переменными.
Если вы хотите сопоставить с образцом существующую переменную, вам необходимо использовать оператор ^/1:
x = 1 case 10 do ^x -> "Won't match" _ -> "Will match" end #=> "Will match"
Использование условий для сопоставления с несколькими значениями
Хотя невозможно сопоставить с несколькими шаблонами в одном разделе, можно сопоставить с несколькими значениями с помощью условий:
case data do
value when value in [:one, :two] ->
"#{value} has been matched"
:three ->
"three has been matched"
end cond(clauses)Source
Оценивает выражение, соответствующее первому разделу, который вычисляет истинное значение.
Примеры
Следующий пример содержит один раздел, который всегда вычисляет истину:
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
Если все разделы вычисляются как nil или false, cond вызывает ошибку. По этой причине может потребоваться добавить заключительный всегда истинный раздел (любое ненулевое false и ненулевое nil ), который всегда будет соответствовать:
cond do
1 + 1 == 1 ->
"This will never match"
2 * 2 != 4 ->
"Nor this"
true ->
"This will"
end
#=> "This will"
Если у вашего выражения cond есть два раздела, и последний возвращается к true, вы можете рассмотреть использование if/2 вместо этого.
fn(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
Доступна начиная с Elixir v1.8.
Хотя опция :into позволяет настроить поведение понимания для заданного типа данных, например, поместив все значения в карту или двоичную строку, этого не всегда достаточно.
Например, предположим, что у вас есть двоичная строка с буквами, где вы хотите подсчитать, сколько раз встречается каждая строчная буква, игнорируя все прописные. Например, для строки "AbCabCABc", мы хотим вернуть карту %{"a" => 1, "b" => 2, "c" => 1}.
Если бы мы использовали :into, нам нужен был бы тип данных, который вычисляет частоту каждого элемента, который он содержит. Хотя такого типа данных в Elixir нет, вы можете реализовать его самостоятельно.
Более простым вариантом было бы использование пониманий для сопоставления и фильтрации букв, а затем вызов Enum.reduce/3 для построения карты, например:
iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}
Хотя приведенное выше решение прямолинейно, у него есть недостаток, заключающийся в том, что данные обрабатываются как минимум дважды. Если ожидаются длинные входные строки, это может быть довольно дорого.
К счастью, понимания также поддерживают опцию :reduce, которая позволила бы нам объединить оба шага в один:
iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...> acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}
Когда задан ключ :reduce, его значение используется в качестве начального аккумулятора, и блок do должен быть изменен на использование -> разделов, где левая часть -> получает накопленное значение предыдущей итерации, а выражение справа должно возвращать новое значение аккумулятора. Как только больше нет элементов, возвращается конечное накопленное значение. Если вообще нет элементов, возвращается начальное значение аккумулятора.
import(module, opts)Source
Импортирует функции и макросы из других модулей.
import/2 позволяет легко получить доступ к функциям или макросам из других модулей без использования квалифицированного имени.
Примеры
Если вы используете несколько функций из данного модуля, вы можете импортировать эти функции и ссылаться на них как на локальные функции, например:
iex> import List iex> flatten([1, [2], 3]) [1, 2, 3]
Селектор
По умолчанию Elixir импортирует функции и макросы из заданного модуля, за исключением тех, которые начинаются с символа подчеркивания (они обычно являются обработчиками):
import List
Разработчик может фильтровать, чтобы импортировать только функции, макросы или сигнилы (которые могут быть функциями или макросами) с помощью опции :only:
import List, only: :functions import List, only: :macros import Kernel, only: :sigils
В качестве альтернативы Elixir позволяет разработчику передавать пары имя/арность в :only или :except для более точного управления тем, что импортировать (или нет):
import List, only: [flatten: 1] import String, except: [split: 2]
Повторный импорт того же модуля удалит предыдущие импорты, за исключением случая, когда используется опция except, которая всегда является исключительной для ранее объявленного import/2. Если нет предыдущего импорта, то он применяется ко всем функциям и макросам в модуле. Например:
import List, only: [flatten: 1, keyfind: 4] import List, except: [flatten: 1]
После двух вызовов импорта будет импортирована только List.keyfind/4.
Функции с подчеркиванием
По умолчанию функции, начинающиеся с _ не импортируются. Если вы действительно хотите импортировать функцию, начинающуюся с _, вы должны явно включить её в селектор :only.
import File.Stream, only: [__build__: 3]
Лексическая область видимости
Важно отметить, что import/2 является лексической. Это означает, что вы можете импортировать определенные макросы внутри определенных функций:
defmodule Math do
def some_function do
# 1) Disable "if/2" from Kernel
import Kernel, except: [if: 2]
# 2) Require the new "if/2" macro from MyMacros
import MyMacros
# 3) Use the new macro
if do_something, it_works
end
end
В примере выше мы импортировали макросы из MyMacros, заменив исходную реализацию if/2 нашей собственной внутри этой конкретной функции. Все остальные функции в этом модуле по-прежнему смогут использовать исходную.
Предупреждения
Если вы импортируете модуль и не используете ни одной из импортированных функций или макросов из этого модуля, Elixir выведет предупреждение, что импорт не используется.
В случае, если импорт генерируется автоматически макросом, Elixir не выведет никаких предупреждений, так как импорт не был явно определен.
Оба поведения с предупреждениями можно изменить, явно задав опцию :warn в true или false.
Неоднозначные имена функций/макросов
Если два модуля A и B импортированы, и оба содержат функцию foo с арностью 1, ошибка выдается только в том случае, если фактически выполняется неоднозначный вызов foo/1; то есть ошибки генерируются лениво, а не жадно.
quote(opts, block)Source
Получает представление любого выражения.
Примеры
iex> quote do
...> sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}
Абстрактное синтаксическое дерево (AST) Elixir
Любой код Elixir может быть представлен с помощью структур данных Elixir. Основной блок Elixir-макросов — кортеж из трёх элементов, например:
{:sum, [], [1, 2, 3]}
Указанный кортеж представляет вызов функции sum с аргументами 1, 2 и 3. Элементы кортежа:
Первый элемент кортежа всегда является атомом или другим кортежем в том же представлении.
Второй элемент кортежа представляет метаданные.
Третий элемент кортежа — аргументы вызова функции. Третий аргумент может быть атомом, который обычно является переменной (или локальным вызовом).
Помимо описанного выше кортежа, Elixir имеет несколько литералов, которые также являются частью его AST. Эти литералы возвращают сами себя при использовании функции quote. Это:
:sum #=> Atoms
1 #=> Integers
2.0 #=> Floats
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
Любое другое значение, например, карта или кортеж из четырёх элементов, должно быть экранировано (Macro.escape/1) перед включением в AST.
Параметры
:bind_quoted— передаёт привязку макросу. При передаче привязкиunquote/1автоматически отключается.:context— устанавливает контекст разрешения.:generated— помечает данный фрагмент как сгенерированный, чтобы не выводить предупреждения. Также полезно для предотвращения сообщений об ошибках dialyzer при генерации макросами неиспользуемых разделов.:file— устанавливает для цитируемых выражений указанный файл.:line— устанавливает для цитируемых выражений указанную строку.:location— при значении:keepсохраняет текущую строку и файл из цитирования. Более подробную информацию см. в разделе «Информация о стеке вызовов» ниже.-
: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 можно использовать во многих случаях и считается хорошей практикой не только потому, что это помогает предотвратить распространённые ошибки, но и потому, что позволяет использовать другие инструменты, предоставляемые макросами, такие как фрагменты unquote, обсуждаемые в некоторых разделах ниже.
Прежде чем мы закончим это краткое введение, вы заметите, что, хотя мы определили переменную 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
require Hygiene
Hygiene.write()
Hygiene.read()
** (CompileError) undefined variable "a" (context Hygiene)
Для этого можно явно передать текущий контекст модуля как аргумент:
defmodule ContextHygiene do
defmacro write do
quote do
var!(a, ContextHygiene) = 1
end
end
defmacro read do
quote do
var!(a, ContextHygiene)
end
end
end
require ContextHygiene
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 и unquote
Не используйте location: :keep в случае, если определение функции также unquote некоторые аргументы макроса. В противном случае Elixir сохранит определение файла текущего местоположения, но цитируемые аргументы могут содержать информацию о строке вызывающего макроса, что приведёт к ошибочным сообщениям об ошибках.
Привязка и фрагменты unquote
Механизмы Elixir quote/unquote предоставляют функциональность фрагментов unquote. Фрагменты unquote обеспечивают лёгкий способ динамической генерации функций. Рассмотрим этот пример:
kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
В примере выше мы динамически сгенерировали функции foo/0 и bar/0. Теперь представьте, что хотим преобразовать эту функциональность в макрос:
defmacro defkv(kv) do
Enum.map(kv, fn {k, v} ->
quote do
def unquote(k)(), do: unquote(v)
end
end)
end
Мы можем вызвать этот макрос так:
defkv [foo: 1, bar: 2]
Однако мы не можем вызвать его так:
kv = [foo: 1, bar: 2] defkv kv
Это происходит потому, что макрос ожидает в качестве аргументов список ключевых слов на этапе компиляции. Поскольку в вышеприведённом примере мы передаём представление переменной kv, наш код даёт сбой.
Это часто встречающаяся проблема при разработке макросов. Мы предполагаем определённую форму макроса. Мы можем обойти это, цитируя переменную внутри цитируемого выражения:
defmacro defkv(kv) do
quote do
Enum.each(unquote(kv), fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
Если вы попытаетесь запустить новый макрос, вы заметите, что он даже не будет компилироваться, потому что переменные k и v не существуют. Это связано с неоднозначностью: unquote(k) может быть фрагментом unquote, как раньше, или обычным unquote, как в unquote(kv).
Одно из решений этой проблемы — отключение цитирования в макросе, но это сделает невозможным внедрение представления 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- процесс должен бесконечно ждать соответствующего сообщения, это то же самое, что и отсутствие пункта after0- если в почтовом ящике нет подходящего сообщения, таймаут произойдёт немедленноположительное целое число, меньшее или равное
4_294_967_295(0xFFFFFFFFв шестнадцатеричном формате) - значение таймаута должно быть представимо в виде беззнакового 32-битного целого числа.
Обработка переменных
Специальная форма receive/1 обрабатывает переменные точно так же, как и специальный макрос case/2. Для получения дополнительной информации, см. документацию для case/2.
require(module, opts)Source
Подключает модуль для использования его макросов.
Примеры
Общедоступные функции в модулях доступны глобально, но для использования макросов необходимо подключить модуль, в котором они определены.
Предположим, вы создали собственную реализацию if/2 в модуле MyMacros. Если вы хотите вызвать её, вам необходимо предварительно подключить модуль MyMacros:
defmodule Math do require MyMacros MyMacros.if do_something, it_works end
Попытка вызвать макрос, который не был загружен, вызовет ошибку.
Сокращение псевдонима
require/2 также принимает :as в качестве опции, чтобы автоматически создать псевдоним. Для получения дополнительной информации, обратитесь к alias/2.
super(args)Source
Вызывает переопределённую функцию при её переопределении с помощью Kernel.defoverridable/1.
См. Kernel.defoverridable/1 для получения дополнительной информации и документации.
try(args)Source
Вычисляет заданные выражения и обрабатывает любые ошибки, выходы или броски, которые могут произойти.
Примеры
try do
do_something_that_may_fail(some_arg)
rescue
ArgumentError ->
IO.puts("Invalid argument given")
catch
value ->
IO.puts("Caught #{inspect(value)}")
else
value ->
IO.puts("Success! The result was #{inspect(value)}")
after
IO.puts("This is printed regardless if it failed or succeeded")
end
Пункт rescue используется для обработки исключений, а пункт catch может использоваться для перехвата брошенных значений и выходов. Пункт else может быть использован для управления потоком в зависимости от результата выражения. catch, rescue, и else пункты работают на основе сопоставления с образцом (подобно специальной форме case).
Вызовы внутри try/1 не являются хвостовой рекурсией, так как виртуальной машине нужно сохранять трассировку стека на случай возникновения исключения. Для получения трассировки стека обратитесь к __STACKTRACE__/0 внутри пункта rescue или catch.
Пункты rescue
Помимо использования сопоставления с образцом, пункты rescue предоставляют удобства для работы с исключениями, позволяя перехватывать исключение по его имени. Все следующие форматы являются допустимыми шаблонами в пунктах rescue:
# Rescue a single exception without binding the exception # to a variable try do UndefinedModule.undefined_function rescue UndefinedFunctionError -> nil end # Rescue any of the given exception without binding try do UndefinedModule.undefined_function rescue [UndefinedFunctionError, ArgumentError] -> nil end # Rescue and bind the exception to the variable "x" try do UndefinedModule.undefined_function rescue x in [UndefinedFunctionError] -> nil end # Rescue all kinds of exceptions and bind the rescued exception # to the variable "x" try do UndefinedModule.undefined_function rescue x -> nil end
Ошибки Erlang
Ошибки Erlang преобразуются в ошибки Elixir при перехвате:
try do :erlang.error(:badarg) rescue ArgumentError -> :ok end #=> :ok
Наиболее распространённые ошибки Erlang преобразуются в их аналоги в Elixir. Те, которые не преобразуются, преобразуются в более общую ErlangError:
try do :erlang.error(:unknown) rescue ErlangError -> :ok end #=> :ok
Фактически, ErlangError можно использовать для перехвата любой ошибки, которая не является правильной ошибкой Elixir. Например, она может быть использована для перехвата ранее возникшей :badarg ошибки до преобразования:
try do :erlang.error(:badarg) rescue ErlangError -> :ok end #=> :ok
Пункты catch
Пункт catch может быть использован для перехвата брошенных значений, выходов и ошибок.
Перехват брошенных значений
catch может быть использован для перехвата значений, брошенных функцией Kernel.throw/1:
try do
throw(:some_value)
catch
thrown_value ->
IO.puts("A value was thrown: #{inspect(thrown_value)}")
end
Перехват значений любого типа
Пункт catch также поддерживает перехват выходов и ошибок. Для этого он позволяет сопоставлять как тип перехваченного значения, так и само значение:
try do
exit(:shutdown)
catch
:exit, value ->
IO.puts("Exited with value #{inspect(value)}")
end
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts("Caught exit or throw with value #{inspect(value)}")
end
Пункт catch также поддерживает :error наряду с :exit и :throw, как и в Erlang, хотя это обычно избегают в пользу механизмов управления raise/rescue. Одна из причин заключается в том, что при перехвате :error, ошибка не преобразуется автоматически в ошибку Elixir:
try do :erlang.error(:badarg) catch :error, :badarg -> :ok end #=> :ok
Пункты after
Пункт after позволяет определять код завершения, который будет вызван как при успешном выполнении блока кода, переданного try/1, так и при возникновении ошибки. Обратите внимание, что процесс обычно завершится при получении сигнала выхода, вызывающего его внезапное завершение, и поэтому пункт after не гарантируется к выполнению. К счастью, большинство ресурсов в Elixir (такие как открытые файлы, таблицы ETS, порты, сокеты и т. д.) связаны с процессом или отслеживают его, и автоматически очищаются, если процесс завершается.
File.write!("tmp/story.txt", "Hello, World")
try do
do_something_with("tmp/story.txt")
after
File.rm("tmp/story.txt")
end
Хотя пункты after вызываются независимо от того, была ошибка или нет, они не изменяют возвращаемое значение. Все следующие примеры возвращают :return_me:
try do
:return_me
after
IO.puts("I will be printed")
:not_returned
end
try do
raise "boom"
rescue
_ -> :return_me
after
IO.puts("I will be printed")
:not_returned
end
Пункты else
Пункты else позволяют сопоставить результат тела, переданного try/1:
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end
Если пункт else отсутствует, и исключения не возникают, возвращается результат выражения:
x = 1
^x =
try do
1 / x
rescue
ArithmeticError ->
:infinity
end
Однако, если пункт else присутствует, но результат выражения не совпадает ни с одним из шаблонов, будет вызвано исключение. Это исключение не будет перехвачено пунктом catch или rescue в том же блоке try:
x = 1
try do
try do
1 / x
rescue
# The TryClauseError cannot be rescued here:
TryClauseError ->
:error_a
else
0 ->
:small
end
rescue
# The TryClauseError is rescued here:
TryClauseError ->
:error_b
end
Аналогично, исключение внутри пункта else не перехватывается или обрабатывается внутри того же блока try:
try do
try do
nil
catch
# The exit(1) call below can not be caught here:
:exit, _ ->
:exit_a
else
_ ->
exit(1)
end
catch
# The exit is caught here:
:exit, _ ->
:exit_b
end
Это означает, что виртуальная машина больше не нуждается в сохранении трассировки стека после попадания в пункт else и, таким образом, возможна хвостовая рекурсия при использовании try с хвостовым вызовом в качестве последнего вызова внутри пункта else (то же самое относится к пунктам rescue и catch).
Только результат вычисленного выражения передаётся в пункт else Если выражение оказывается в пунктах rescue или catch, их результаты не передаются в else:
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end
Обработка переменных
Поскольку выражение внутри try может не быть вычислено из-за исключения, любая переменная, созданная внутри try, недоступна снаружи. Например:
try do x = 1 do_something_that_may_fail(same_arg) :ok catch _, _ -> :failed end x #=> unbound variable "x"
В приведённом примере x недоступно, так как оно было определено внутри пункта try. Общим подходом к решению этой проблемы является возврат переменных, определённых внутри try:
x =
try do
x = 1
do_something_that_may_fail(same_arg)
x
catch
_, _ -> :failed
end unquote(expr)Source
Раскладывает данное выражение внутри цитируемого выражения.
Эта функция ожидает допустимое выражение AST Elixir, также известное как цитируемое выражение, в качестве аргумента. Если вы хотите unquote какое-либо значение, например, карту или четырёхэлементную кортеж, вы должны вызвать Macro.escape/1 перед раскладыванием.
Примеры
Представьте ситуацию, когда у вас есть цитируемое выражение, и вы хотите вставить его внутри другого цитируемого выражения. Первая попытка была бы:
value =
quote do
13
end
quote do
sum(1, value, 3)
end
В результате аргумент для вызова функции :sum не является ожидаемым результатом:
{:sum, [], [1, {:value, [], Elixir}, 3]}
Для этого мы используем unquote:
iex> value =
...> quote do
...> 13
...> end
iex> quote do
...> sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}
Если вы хотите раскладывать значение, которое не является цитируемым выражением, например, карту, вам нужно вызвать Macro.escape/1 перед этим:
iex> value = %{foo: :bar}
iex> quote do
...> process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}
Если вы забудете выполнить экранирование, Elixir выдаст ошибку при компиляции кода.
unquote_splicing(expr)Source
Раскладывает заданный список, расширяя его аргументы.
Аналогично unquote/1.
Примеры
iex> values = [2, 3, 4]
iex> quote do
...> sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]} with(args)Source
Объединяет совпадающие разделы.
Один из способов понять with — показать, какие шаблоны кода он улучшает. Представьте, что у вас есть карта, где поля width и height являются необязательными, и вы хотите вычислить её площадь, как {:ok, area} или вернуть :error. Мы могли бы реализовать эту функцию как:
def area(opts) do
case Map.fetch(opts, :width) do
{:ok, width} ->
case Map.fetch(opts, :height) do
{:ok, height} -> {:ok, width * height}
:error -> :error
end
:error ->
:error
end
end
при вызове как area(%{width: 10, height: 15}), она должна вернуть {:ok, 150}. Если какое-либо из полей отсутствует, она возвращает :error.
Хотя код выше работает, он довольно громоздкий. Используя with, мы могли бы переписать его как:
def area(opts) do
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height) do
{:ok, width * height}
end
end
Вместо определения вложенных case с разделами, мы используем with вместе с оператором PATTERN <- EXPRESSION для сопоставления выражений с правой стороны с шаблоном слева. Рассматривайте <- как аналог =, за исключением того, что, в то время как = вызывает исключение в случае несовпадения, <- просто прерывает цепочку with и возвращает несоответствующее значение.
Давайте попробуем это в IEx:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{:ok, 150}
Если все разделы соответствуют, блок do выполняется, возвращая его результат. В противном случае цепочка прерывается, и возвращается несоответствующее значение:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
:error
Ограничения также могут быть использованы в шаблонах:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"}
Как и в for/1, переменные, связанные внутри with/1, недоступны за пределами with/1.
Выражения без <- также могут быть использованы в разделах. Например, вы можете выполнять обычные сопоставления с оператором =:
iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil
Поведение любого выражения в разделе такое же, как если бы оно было написано за пределами with. Например, = вызовет MatchError вместо возврата несоответствующего значения:
with :foo = :bar, do: :ok ** (MatchError) no match of right hand side value: :bar
Как и при любом другом вызове функции или макроса в Elixir, явные скобки также могут быть использованы вокруг аргументов перед блоком do-end:
iex> opts = %{width: 10, height: 15}
iex> with(
...> {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...> {:ok, width * height}
...> end
{:ok, 150}
Выбор между скобками и их отсутствием зависит от предпочтений.
Разделы else
Вариант else может быть задан для изменения того, что возвращается из with в случае неудачного сопоставления:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
Блок else работает как case: он может содержать несколько разделов, и первое совпадение будет использовано. Переменные, связанные внутри with (например, width в данном примере), недоступны в блоке else.
Если блок else используется, и нет совпадающих разделов, возникает исключение WithClauseError.
Внимание!
Следует помнить, что одним из потенциальных недостатков with является то, что все разделы с ошибками выравниваются в один блок else. Например, рассмотрите этот код, который проверяет, указывает ли заданный путь на файл Elixir и существует ли он перед созданием резервной копии:
with ".ex" <- Path.extname(path),
true <- File.exists?(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
else
binary when is_binary(binary) ->
{:error, :invalid_extension}
false ->
{:error, :missing_file}
end
Обратите внимание, как мы должны восстанавливать типы результатов Path.extname/1 и File.exists?/1, чтобы создать сообщения об ошибках. В этом случае лучше переработать код таким образом, чтобы каждый <- уже возвращал желаемый формат в случае ошибок, как это:
with :ok <- validate_extension(path),
:ok <- validate_exists(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
end
defp validate_extension(path) do
if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end
defp validate_exists(path) do
if File.exists?(path), do: :ok, else: {:error, :missing_file}
end
Обратите внимание, как код выше организован лучше и понятнее, как только мы убедимся, что каждый <- в with возвращает нормализованный формат.
^varSource
Оператор закрепления. Доступ к уже связанной переменной в разделах сопоставления.
Примеры
Elixir позволяет переменным пересвязываться посредством статического однократного присваивания:
iex> x = 1 iex> x = x + 1 iex> x 2
Однако в некоторых ситуациях полезно сопоставить существующее значение, а не пересвязывать. Это можно сделать с помощью специальной формы ^, разговорно известной как оператор закрепления:
iex> x = 1 iex> ^x = List.first([1]) iex> ^x = List.first([2]) ** (MatchError) no match of right hand side value: 2
Обратите внимание, что ^x всегда ссылается на значение x до сопоставления. Следующий пример будет совпадать:
iex> x = 0
iex> {x, ^x} = {1, 0}
iex> x
1 {args}Source
Создаёт кортеж.
Дополнительную информацию о типе данных кортежей и функциях для работы с кортежами можно найти в модуле Tuple; некоторые функции для работы с кортежами также доступны в Kernel (например, Kernel.elem/2 или Kernel.tuple_size/1).
Представление AST
Только кортежи из двух элементов считаются литералами в Elixir и возвращают себя при цитировании. Поэтому все остальные кортежи представляются в AST как вызовы специальной формы :{}.
iex> quote do
...> {1, 2}
...> end
{1, 2}
iex> quote do
...> {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/Kernel.SpecialForms.html