Исходный код 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
Также доступна операция обновления, специфичная для структур:
%User{user | age: 28}
Преимущества структур заключаются в том, что они проверяют, являются ли заданные ключи частью определенной структуры. Следующий пример завершится ошибкой, так как в структуре User нет ключа :full_name.
%User{full_name: "john doe"}
Приведенный синтаксис гарантирует, что заданные ключи являются допустимыми на этапе компиляции, и гарантирует на этапе выполнения, что заданный аргумент является структурой, в противном случае будет ошибка BadStructError.
Хотя структуры являются картами, по умолчанию структуры не реализуют ни одного из протоколов, реализованных для карт. Смотрите Kernel.defprotocol/2 для получения дополнительной информации о том, как структуры могут использоваться с протоколами для полиморфного диспетчера. Также см. Kernel.struct/2 и Kernel.struct!/2 для примеров создания и обновления структур динамически.
Сопоставление с образцом по именам структур
Помимо возможности сопоставления с образцом по полям структуры, например:
%User{age: age} = user
Структуры также позволяют сопоставлять с образцом имя структуры:
%struct_name{} = user
struct_name #=> User
Вы также можете назначить имя структуры _, когда вы хотите проверить, является ли что-то структурой, но не интересуетесь ее именем:
%_{} = user %{}Source
Создает карту.
См. модуль Map для получения дополнительной информации о картах, их синтаксисе и способах доступа к ним и их изменения.
Представление AST
Независимо от того, используется ли => или синтаксис ключевых слов, пары ключ-значение в картах всегда представляются внутри в виде списка кортежей из двух элементов для простоты:
iex> quote do
...> %{"a" => :b, c: :d}
...> end
{:%{}, [], [{"a", :b}, {:c, :d}]} &exprSource
Оператор захвата. Захватывает или создает анонимную функцию.
Захват
Оператор захвата чаще всего используется для захвата функции с заданным именем и арностью из модуля:
iex> fun = &Kernel.is_atom/1
iex> fun.(:atom)
true
iex> fun.("string")
false
В приведенном примере мы захватили Kernel.is_atom/1 как анонимную функцию, а затем вызвали ее.
Оператор захвата также можно использовать для захвата локальных функций, в том числе частных, и импортированных функций, опуская имя модуля:
&local_function/1
См. также Function.capture/3.
Анонимные функции
Оператор захвата также можно использовать для частичного применения функций, где &1, &2 и так далее могут использоваться в качестве заполнительных значений. Например:
iex> double = &(&1 * 2) iex> double.(2) 4
Другими словами, &(&1 * 2) эквивалентно fn x -> x * 2 end.
Мы можем частично применить удаленную функцию с заполнителем:
iex> take_five = &Enum.take(&1, 5) iex> take_five.(1..10) [1, 2, 3, 4, 5]
Еще один пример с использованием импортированной или локальной функции:
iex> first_elem = &elem(&1, 0)
iex> first_elem.({0, 1})
0
Оператор & может использоваться с более сложными выражениями:
iex> fun = &(&1 + &2 + &3) iex> fun.(1, 2, 3) 6
А также со списками и кортежами:
iex> fun = &{&1, &2}
iex> fun.(1, 2)
{1, 2}
iex> fun = &[&1 | &2]
iex> fun.(1, [2, 3])
[1, 2, 3]
Единственные ограничения при создании анонимных функций заключаются в том, что должно присутствовать как минимум один заполнители, т. е. он должен содержать по меньшей мере &1, и что выражения блоков не поддерживаются:
# No placeholder, fails to compile. &(:foo) # Block expression, fails to compile. &(&1; &2)
left . rightSource
Оператор точки. Определяет удаленный вызов, вызов анонимной функции или псевдоним.
Точка (.) в Elixir может использоваться для удаленных вызовов:
iex> String.downcase("FOO")
"foo"
В этом примере мы использовали . для вызова downcase в модуле String, передавая "FOO" в качестве аргумента.
Точка также может использоваться для вызова анонимных функций:
iex> (fn n -> n end).(7) 7
в котором есть функция в левой части.
Мы также можем использовать точку для создания псевдонимов:
iex> Hello.World Hello.World
На этот раз мы объединили два псевдонима, определив окончательный псевдоним Hello.World.
Синтаксис
Правая часть . может быть словом, начинающимся с большой буквы, которое представляет псевдоним, словом, начинающимся с маленькой буквы или подчеркивания, любым допустимым оператором языка или любым именем, заключенным в одинарные или двойные кавычки. Вот все допустимые примеры:
iex> Kernel.Sample Kernel.Sample iex> Kernel.length([1, 2, 3]) 3 iex> Kernel.+(1, 2) 3 iex> Kernel."+"(1, 2) 3
Оборачивание имени функции в одинарные или двойные кавычки всегда является удаленным вызовом. Таким образом, Kernel."Foo" будет пытаться вызвать функцию "Foo", а не возвращать псевдоним Kernel.Foo. Это сделано по умолчанию, так как имена модулей более строгие, чем имена функций.
Когда точка используется для вызова анонимной функции, есть только один операнд, но он все равно записывается с использованием постфиксной нотации:
iex> negate = fn n -> -n end iex> negate.(7) -7
Выражение в кавычках
Когда . используется, выражение в кавычках может принимать две различные формы. Когда правая часть начинается с маленькой буквы (или подчеркивания):
iex> quote do
...> String.downcase("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Обратите внимание, что у нас есть внутренний кортеж, содержащий атом :., представляющий точку в качестве первого элемента:
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
Этот кортеж следует общему структуре выражения в кавычках в Elixir, с именем в качестве первого аргумента, некоторым списком ключевых слов в качестве метаданных как вторым, и списком аргументов как в третьем. В данном случае аргументами являются псевдоним String и атом :downcase. Второй аргумент в удаленном вызове — всегда атом.
В случае вызовов анонимных функций внутренний кортеж со специальной формой точки имеет только один аргумент, отражающий тот факт, что оператор является унарным:
iex> quote do
...> negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}
В случае, если правая часть является псевдонимом (т. е. начинается с большой буквы), мы получаем:
iex> quote do
...> Hello.World
...> end
{:__aliases__, [alias: false], [:Hello, :World]}
Мы более подробно рассматриваем псевдонимы в документации специальной формы __aliases__/1.
Расквычивание
Мы также можем использовать расквычивание для генерации удаленного вызова в выражении в кавычках:
iex> x = :downcase
iex> quote do
...> String.unquote(x)("FOO")
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Аналогично Kernel."FUNCTION_NAME", unquote(x) всегда будет генерировать удаленный вызов, независимо от значения x. Чтобы сгенерировать псевдоним через выражение в кавычках, необходимо использовать Module.concat/2:
iex> x = Sample
iex> quote do
...> Module.concat(String, unquote(x))
...> end
{{:., [], [{:__aliases__, [alias: false], [:Module]}, :concat]}, [],
[{:__aliases__, [alias: false], [:String]}, Sample]} __aliases__(args)Source
Внутренняя специальная форма для хранения информации о псевдонимах.
Обычно она компилируется в атом:
iex> quote do
...> Foo.Bar
...> end
{:__aliases__, [alias: false], [:Foo, :Bar]}
Elixir представляет Foo.Bar как __aliases__, чтобы вызовы могли быть однозначно идентифицированы по оператору :.. Например:
iex> quote do
...> Foo.bar()
...> end
{{:., [], [{:__aliases__, [alias: false], [:Foo]}, :bar]}, [], []}
Всякий раз, когда итератор выражений видит :. в качестве ключа кортежа, он может быть уверен, что это представляет вызов, и второй аргумент в списке — это атом.
С другой стороны, псевдонимы обладают некоторыми свойствами:
Первый элемент псевдонимов может быть любым термином, который должен расширяться до атома на этапе компиляции.
Следующие элементы псевдонимов гарантированно всегда будут атомами.
Если первый элемент псевдонимов является атомом
: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 бинарным 16, 32 и 64 соответственно.
Для бинарных данных по умолчанию используется размер бинарных данных. Только последний бинарный объект в совпадении может использовать размер по умолчанию. Все остальные должны иметь свой размер, указанный явно, даже если совпадение однозначно. Например:
iex> <<name::binary-size(5), " the ", species::binary>> = <<"Frank the Walrus">>
"Frank the Walrus"
iex> {name, species}
{"Frank", "Walrus"}
Размер может быть переменной или любым допустимым выражением условия:
iex> name_size = 5
iex> <<name::binary-size(^name_size), " the ", species::binary>> = <<"Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
Размер может ссылаться на предыдущие переменные, определенные в самой битовой строке:
iex> <<name_size::size(8), name::binary-size(name_size), " the ", species::binary>> = <<5, "Frank the Walrus">>
iex> {name, species}
{"Frank", "Walrus"}
Однако он не может ссылаться на переменные, определенные в совпадении за пределами бинарных/битовых строк:
{name_size, <<name::binary-size(name_size), _rest::binary>>} = {5, <<"Frank the Walrus">>}
** (CompileError): undefined variable "name_size" in bitstring segment
Не указание размера для не последнего элемента приводит к ошибке компиляции:
<<name::binary, " the ", species::binary>> = <<"Frank the Walrus">> ** (CompileError): a binary field without size is only allowed at the end of a binary pattern
Сокращенная запись
Размер и единица также могут быть указаны с помощью сокращенной записи при передаче целочисленных значений:
iex> x = 1 iex> <<x::8>> == <<x::size(8)>> true iex> <<x::8*4>> == <<x::size(8)-unit(4)>> true
Эта запись отражает тот факт, что эффективный размер определяется путем умножения размера на единицу.
Модификаторы
У некоторых типов есть связанные модификаторы, которые позволяют устранить неоднозначность в представлении байтов.
| Модификатор | Применимый тип(ы) |
|---|---|
signed |
integer |
unsigned (по умолчанию) |
integer |
little |
integer, float, utf16, utf32
|
big (по умолчанию) |
integer, float, utf16, utf32
|
native |
integer, float, utf16, utf32
|
Знак
Целые числа могут быть signed или unsigned, по умолчанию unsigned.
iex> <<int::integer>> = <<-100>> <<156>> iex> int 156 iex> <<int::integer-signed>> = <<-100>> <<156>> iex> int -100
signed и unsigned используются только для сопоставления бинарных данных (см. ниже) и только для целых чисел.
iex> <<-100::signed, _rest::binary>> = <<-100, "foo">> <<156, 102, 111, 111>>
Порядок байтов
В Elixir есть три варианта порядка байтов: big, little, и native. По умолчанию big:
iex> <<number::little-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 256 iex> <<number::big-integer-size(16)>> = <<0, 1>> <<0, 1>> iex> number 1
native определяется виртуальной машиной при запуске и зависит от операционной системы хоста.
Сопоставление бинарных/битовых строк
Сопоставление бинарных данных — мощная функция в Elixir, полезная для извлечения информации из бинарных данных и сопоставления с образцом.
Сопоставление бинарных данных может использоваться само по себе для извлечения информации из бинарных данных:
iex> <<"Hello, ", place::binary>> = "Hello, World" "Hello, World" iex> place "World"
Или как часть определений функций для сопоставления с образцом:
defmodule ImageType do
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, _rest::binary>>), do: :png
def type(<<@jpg_signature, _rest::binary>>), do: :jpg
def type(_), do: :unknown
end
Производительность и оптимизации
Компилятор Erlang может предоставить ряд оптимизаций для создания и сопоставления бинарных данных. Для просмотра выходных данных оптимизации установите опцию компилятора bin_opt_info:
ERL_COMPILER_OPTIONS=bin_opt_info mix compile
Для получения дополнительной информации о конкретных оптимизациях и аспектах производительности см. главу "Построение и сопоставление бинарных данных" в Руководстве по производительности Erlang.
left = rightSource
Оператор сопоставления. Сопоставляет значение справа с шаблоном слева.
alias(модуль, параметры)Source
alias/2 используется для создания псевдонимов, часто полезных с именами модулей.
Примеры
alias/2 может быть использован для создания псевдонима для любого модуля:
defmodule Math do alias MyKeyword, as: Keyword end
В примере выше, мы создали псевдоним для MyKeyword как Keyword. Теперь любая ссылка на Keyword будет автоматически заменена на MyKeyword.
В случае, если нужно получить доступ к исходному Keyword, можно сделать это, обратившись к Elixir:
Keyword.values #=> uses MyKeyword.values Elixir.Keyword.values #=> uses Keyword.values
Обратите внимание, что вызов alias без опции :as автоматически создаёт псевдоним, основанный на последней части модуля. Например:
alias Foo.Bar.Baz
Это то же самое, что:
alias Foo.Bar.Baz, as: Baz
Мы также можем создать псевдонимы для нескольких модулей в одной строке:
alias Foo.{Bar, Baz, Biz}
Это то же самое, что:
alias Foo.Bar alias Foo.Baz alias Foo.Biz
Лексическая область видимости
import/2, require/2 и alias/2 называются директивами и все имеют лексическую область видимости. Это означает, что вы можете создавать псевдонимы внутри определённых функций, и это не повлияет на общую область видимости.
Предупреждения
Если вы создаёте псевдоним модуля и не используете его, Elixir выведет предупреждение, предполагая, что псевдоним не используется.
В случае, если псевдоним генерируется автоматически макросом, Elixir не выводит никаких предупреждений, так как псевдоним не был явно определён.
Оба поведения предупреждений можно изменить, явно установив опцию :warn в true или false.
case(условие, пункты)Source
Сопоставляет данное выражение с данными пунктами.
case/2 опирается на сопоставление с образцом и условия для выбора пункта для выполнения. Если ваша логика не может быть выражена в рамках образцов и условий, рассмотрите использование if/2 или cond/1 вместо этого.
Примеры
case File.read(file) do
{:ok, contents} when is_binary(contents) ->
String.split(contents, "\n")
{:error, _reason} ->
Logger.warning "could not find #{file}, assuming empty..."
[]
end
В примере выше, мы сопоставляем результат File.read/1 с каждым пунктом "заголовок" и выполняем пункт "тело", соответствующий первому пункту, который совпадает.
Если ни один пункт не совпадает, генерируется ошибка. По этой причине может потребоваться добавить окончательный пункт "ловушку" (например, _), который всегда будет совпадать.
x = 10
case x do
0 ->
"This clause won't match"
_ ->
"This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"
Если вы обнаружите себя вложенными выражениями case внутри выражений case, рассмотрите использование with/1.
Обработка переменных
Обратите внимание, что переменные, связанные в пункте, не утекают во внешнюю область видимости:
case data do
{:ok, value} -> value
:error -> nil
end
value
#=> unbound variable value
Переменные во внешней области видимости также не могут быть перезаписаны:
value = 7 case lucky? do false -> value = 13 true -> true end value #=> 7
В примере выше, value будет 7 независимо от значения lucky?. Переменная value , связанная в пункте, и переменная value , связанная во внешней области видимости, являются двумя совершенно отдельными переменными.
Если вы хотите сопоставить с образцом с существующей переменной, вам нужно использовать оператор ^/1:
x = 1 case 10 do ^x -> "Won't match" _ -> "Will match" end #=> "Will match"
Использование условий для сопоставления с несколькими значениями
Хотя невозможно сопоставить с образцом с несколькими образцами в одном пункте, можно сопоставить с несколькими значениями, используя условия:
case data do
value when value in [:one, :two] ->
"#{value} has been matched"
:three ->
"three has been matched"
end cond(пункты)Source
Вычисляет выражение, соответствующее первому пункту, который вычисляется в истинное значение.
Примеры
Следующий пример содержит один пункт, который всегда вычисляется в истинное значение:
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
Если все пункты вычисляются в nil или false, cond генерирует ошибку. По этой причине может потребоваться добавить окончательное всегда истинное условие (любое не-false и не-nil), которое всегда будет совпадать:
cond do
1 + 1 == 1 ->
"This will never match"
2 * 2 != 4 ->
"Nor this"
true ->
"This will"
end
#=> "This will"
Если ваш cond имеет два пункта, и последний переходит к true, вы можете рассмотреть использование if/2 вместо этого.
fn(пункты)Source
Определяет анонимную функцию.
См. Function для получения дополнительной информации.
Примеры
iex> add = fn a, b -> a + b end iex> add.(1, 2) 3
Анонимные функции также могут иметь несколько пунктов. Все пункты должны ожидать одинаковое количество аргументов:
iex> negate = fn ...> true -> false ...> false -> true ...> end iex> negate.(false) true
for(args)Source
Понимания позволяют быстро создавать структуру данных из перечисляемого объекта или битовой строки.
Начнём с примера:
iex> for n <- [1, 2, 3, 4], do: n * 2 [2, 4, 6, 8]
Понимание принимает множество генераторов и фильтров. for использует оператор <- для извлечения значений из перечисляемого объекта справа и сопоставления их с шаблоном слева. Мы называем их генераторами:
# A list generator: iex> for n <- [1, 2, 3, 4], do: n * 2 [2, 4, 6, 8] # A comprehension with two generators iex> for x <- [1, 2], y <- [2, 3], do: x * y [2, 3, 4, 6]
Также можно задать фильтры:
# A comprehension with a generator and a filter iex> for n <- [1, 2, 3, 4, 5, 6], rem(n, 2) == 0, do: n [2, 4, 6]
Фильтры должны вычисляться к истинному значению (всё кроме nil и false). Если фильтр ложный, то текущее значение отбрасывается.
Генераторы также могут использоваться для фильтрации, так как они удаляют любые значения, которые не соответствуют шаблону слева от <-:
iex> users = [user: "john", admin: "meg", guest: "barbara"]
iex> for {type, name} when type != :guest <- users do
...> String.upcase(name)
...> end
["JOHN", "MEG"]
Также поддерживаются генераторы битовых строк, которые очень полезны при необходимости организации потоков битовых строк:
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
Присваивания переменных внутри понимания, будь то в генераторах, фильтрах или внутри блока, не отражаются за пределами понимания.
Присваивания переменных внутри фильтров также должны возвращать истинное значение, в противном случае значения отбрасываются. Давайте рассмотрим пример. Представьте, что у вас есть список ключевых слов, где ключ — это язык программирования, а значение — его непосредственный родитель. Затем давайте попробуем вычислить прародителя каждого языка. Вы можете попробовать это:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent = languages[parent], do: {language, grandparent}
[elixir: :prolog]
Учитывая, что прародителями Erlang и Prolog были nil, эти значения были отфильтрованы. Если вы не хотите этого поведения, простым вариантом является перемещение фильтра внутрь блока do:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages do
...> grandparent = languages[parent]
...> {language, grandparent}
...> end
[elixir: :prolog, erlang: nil, prolog: nil]
Однако такой вариант не всегда доступен, так как могут быть дополнительные фильтры. Альтернативой является преобразование фильтра в генератор, заключив правую часть = в список:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
iex> for {language, parent} <- languages, grandparent <- [languages[parent]], do: {language, grandparent}
[elixir: :prolog, erlang: nil, prolog: nil]
Параметры :into и :uniq
В приведённых выше примерах результатом, возвращаемым пониманием, всегда был список. Возвращаемый результат можно настроить, передав параметр :into, который принимает любую структуру, если она реализует протокол Collectable.
Например, мы можем использовать генераторы битовых строк с параметром :into для лёгкого удаления всех пробелов в строке:
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>> "helloworld"
Модуль IO предоставляет потоки, которые являются как Enumerable, так и Collectable, вот эхо-сервер для преобразования в верхний регистр с использованием пониманий:
for line <- IO.stream(), into: IO.stream() do String.upcase(line) end
Аналогично, uniq: true также может быть задан для пониманий, чтобы гарантировать, что результаты добавляются в коллекцию только в том случае, если они ранее не возвращались. Например:
iex> for x <- [1, 1, 2, 3], uniq: true, do: x * 2 [2, 4, 6] iex> for <<x <- "abcabc">>, uniq: true, into: "", do: <<x - 32>> "ABC"
Параметр :reduce
Доступен начиная с Elixir v1.8.
В то время как параметр :into позволяет настроить поведение понимания для определённого типа данных, например, поместить все значения в карту или в двоичную строку, этого не всегда достаточно.
Например, представьте, что у вас есть двоичная строка с буквами, где вы хотите подсчитать, сколько раз встречается каждая строчная буква, игнорируя все заглавные. Например, для строки "AbCabCABc", мы хотим вернуть карту %{"a" => 1, "b" => 2, "c" => 1}.
Если бы мы использовали :into, нам понадобился бы тип данных, вычисляющий частоту каждого элемента, который он содержит. В Elixir такого типа данных нет, но вы можете реализовать его самостоятельно.
Простым вариантом было бы использование пониманий для сопоставления и фильтрации букв, а затем вызов Enum.reduce/3 для построения карты, например:
iex> letters = for <<x <- "AbCabCABc">>, x in ?a..?z, do: <<x>>
iex> Enum.reduce(letters, %{}, fn x, acc -> Map.update(acc, x, 1, & &1 + 1) end)
%{"a" => 1, "b" => 2, "c" => 1}
Хотя вышеприведённый способ прямой, у него есть недостаток: данные обрабатываются как минимум дважды. Если вы ожидаете длинные строки в качестве входных данных, это может быть довольно дорого.
К счастью, понимания также поддерживают параметр :reduce, который позволил бы нам объединить оба шага выше в один шаг:
iex> for <<x <- "AbCabCABc">>, x in ?a..?z, reduce: %{} do
...> acc -> Map.update(acc, <<x>>, 1, & &1 + 1)
...> end
%{"a" => 1, "b" => 2, "c" => 1}
Когда указан ключ :reduce, его значение используется в качестве начального аккумулятора, а блок do должен быть изменён на использование -> инструкций, где левая часть -> получает накопленное значение предыдущей итерации, а выражение справа должно возвращать новое значение аккумулятора. После того, как все элементы обработаны, возвращается итоговое значение аккумулятора. Если элементов вообще нет, возвращается начальное значение аккумулятора.
import(module, opts)Source
Импортирует функции и макросы из других модулей.
import/2 позволяет легко получить доступ к функциям или макросам из других модулей без использования квалифицированного имени.
Примеры
Если вы используете несколько функций из данного модуля, вы можете импортировать эти функции и ссылаться на них как на локальные функции, например:
iex> import List iex> flatten([1, [2], 3]) [1, 2, 3]
Селектор
По умолчанию Elixir импортирует функции и макросы из данного модуля, за исключением тех, которые начинаются с нижнего подчёркивания (обычно это колбэки):
import List
Разработчик может отфильтровать импорт, чтобы импортировать только функции, макросы или сигилы (которые могут быть функциями или макросами), через параметр :only:
import List, only: :functions import List, only: :macros import Kernel, only: :sigils
В качестве альтернативы, Elixir позволяет разработчику передать пары имя/арность параметрам :only или :except для тонкого управления тем, что импортировать (или не импортировать):
import List, only: [flatten: 1] import String, except: [split: 2]
Повторный импорт одного и того же модуля удалит предыдущие импорты, за исключением случая использования параметра except, который всегда является исключительным по отношению к ранее объявленному import/2. Если нет предыдущего импорта, то он применяется ко всем функциям и макросам в модуле. Например:
import List, only: [flatten: 1, keyfind: 4] import List, except: [flatten: 1]
После двух вызовов импорта будет импортирована только функция List.keyfind/4.
Функции с нижним подчёркиванием
По умолчанию функции, начинающиеся с _, не импортируются. Если вам действительно нужно импортировать функцию, начинающуюся с _, вы должны явно включить её в селектор :only.
import File.Stream, only: [__build__: 3]
Лексическая область видимости
Важно отметить, что import/2 является лексической. Это означает, что вы можете импортировать определённые макросы внутри определённых функций:
defmodule Math do
def some_function do
# 1) Disable "if/2" from Kernel
import Kernel, except: [if: 2]
# 2) Require the new "if/2" macro from MyMacros
import MyMacros
# 3) Use the new macro
if do_something, it_works
end
end
В приведённом примере мы импортировали макросы из MyMacros, заменив исходную реализацию if/2 своей собственной внутри этой конкретной функции. Все остальные функции в этом модуле по-прежнему смогут использовать исходную реализацию.
Предупреждения
Если вы импортируете модуль и не используете ни одну из импортированных функций или макросов из этого модуля, Elixir выведет предупреждение, что импорт не используется.
В случае если импорт генерируется автоматически макросом, Elixir не выводит никаких предупреждений, так как импорт не был явно определён.
Оба поведения предупреждений могут быть изменены, явно установив параметр :warn в true или false.
Неоднозначные имена функций/макросов
Если два модуля A и B импортированы, и оба содержат функцию foo с арностью 1, ошибка выводится только в том случае, если фактически выполняется неоднозначный вызов foo/1; то есть, ошибки выводятся лениво, а не жадно.
quote(opts, block)Source
Получает представление любого выражения.
Примеры
iex> quote do
...> sum(1, 2, 3)
...> end
{:sum, [], [1, 2, 3]}
Абстрактное синтаксическое дерево (AST) Elixir
Любой код Elixir можно представить с помощью структур данных Elixir. Основным блоком Elixir-макросов является кортеж из трёх элементов, например:
{:sum, [], [1, 2, 3]}
Кортеж выше представляет вызов функции sum, передавая 1, 2 и 3 в качестве аргументов. Элементы кортежа:
Первый элемент кортежа всегда атом или другой кортеж в таком же представлении.
Второй элемент кортежа представляет метаданные.
Третий элемент кортежа — аргументы для вызова функции. Третий аргумент может быть атомом, который обычно является переменной (или локальным вызовом).
Помимо описанного выше кортежа, в Elixir есть несколько литералов, которые также являются частью его AST. Эти литералы возвращают сами себя при цитировании. Это:
:sum #=> Atoms
1 #=> Integers
2.0 #=> Floats
[1, 2] #=> Lists
"strings" #=> Strings
{key, value} #=> Tuples with two elements
Любое другое значение, такое как карта или кортеж из четырёх элементов, должно быть экранировано (Macro.escape/1) перед включением в AST.
Параметры
:bind_quoted— передаёт привязку макросу. В случае предоставления привязкиunquote/1автоматически отключается.:context— устанавливает контекст разрешения.:generated— отмечает заданный фрагмент как сгенерированный, чтобы не выводить предупреждения. Это также полезно для предотвращения сообщений об ошибках dialyzer при генерации макросами неиспользуемых разделах.:file— устанавливает файл для цитируемых выражений.:line— устанавливает строку для цитируемых выражений.:location— когда установлено в:keep, сохраняет текущую строку и файл из quote. См. раздел «Информация о трассировке стека» ниже для получения дополнительной информации.-
:unquote— когдаfalse, отключает unquoting. Это означает, что любой вызовunquoteбудет сохранён в AST как есть, вместо замены аргументамиunquote. Например:iex> quote do ...> unquote("hello") ...> end "hello" iex> quote unquote: false do ...> unquote("hello") ...> end {:unquote, [], ["hello"]}
Quote и макросы
quote/2 обычно используется с макросами для генерации кода. В качестве упражнения, давайте определим макрос, который умножает число на себя (в квадрат). На практике нет необходимости определять такой макрос (и это, на самом деле, плохая практика), но он достаточно простой, чтобы сфокусироваться на важных аспектах quote и макросов:
defmodule Math do
defmacro squared(x) do
quote do
unquote(x) * unquote(x)
end
end
end
Его можно вызвать как:
import Math
IO.puts("Got #{squared(5)}")
Сначала в этом примере нет ничего, что явно указывает на то, что это макрос. Но происходит то, что во время компиляции squared(5) становится 5 * 5. Аргумент 5 дублируется в генерируемом коде. Мы можем увидеть это поведение на практике, потому что у нашего макроса есть ошибка:
import Math
my_number = fn ->
IO.puts("Returning 5")
5
end
IO.puts("Got #{squared(my_number.())}")
В приведенном выше примере будет выведено:
Returning 5 Returning 5 Got 25
Обратите внимание, как «Возвращает 5» выведено дважды вместо одного. Это связано с тем, что макрос получает выражение, а не значение (что мы ожидали бы от обычной функции). Это означает, что:
squared(my_number.())
Фактически расширяется до:
my_number.() * my_number.()
Что вызывает функцию дважды, объясняя, почему мы получаем выводимое значение дважды! В большинстве случаев это нежелательное поведение, и поэтому одно из первых вещей, которые нужно иметь в виду при работе с макросами, — это **не цитировать одно и то же значение более одного раза**.
Давайте исправим наш макрос:
defmodule Math do
defmacro squared(x) do
quote do
x = unquote(x)
x * x
end
end
end
Теперь вызов squared(my_number.()) как и раньше выведет значение только один раз.
На самом деле, этот шаблон настолько распространён, что в большинстве случаев вы захотите использовать опцию bind_quoted с quote/2:
defmodule Math do
defmacro squared(x) do
quote bind_quoted: [x: x] do
x * x
end
end
end
:bind_quoted будет транслироваться в тот же код, что и в примере выше. :bind_quoted может быть использован во многих случаях и считается хорошей практикой, не только потому, что это помогает избежать распространённых ошибок, но и потому, что позволяет использовать другие инструменты, предоставляемые макросами, такие как фрагменты unquote, обсуждаемые в некоторых разделах ниже.
Прежде чем закончить это краткое введение, вы заметите, что, даже если мы определили переменную x внутри quote:
quote do x = unquote(x) x * x end
Когда мы вызываем:
import Math squared(5) x ** (CompileError) undefined variable "x"
Мы видим, что x не просочился в контекст пользователя. Это происходит потому, что Elixir-макросы гигиеничны, тема, которую мы подробно обсудим в следующих разделах.
Гигиена в переменных
Рассмотрим следующий пример:
defmodule Hygiene do
defmacro no_interference do
quote do
a = 1
end
end
end
require Hygiene
a = 10
Hygiene.no_interference()
a
#=> 10
В примере выше a возвращает 10, даже если макрос, по-видимому, устанавливает его в 1, потому что переменные, определённые в макросе, не влияют на контекст, в котором макрос выполняется. Если вы хотите установить или получить переменную в контексте вызывающей функции, вы можете сделать это с помощью макроса var!:
defmodule NoHygiene do
defmacro interference do
quote do
var!(a) = 1
end
end
end
require NoHygiene
a = 10
NoHygiene.interference()
a
#=> 1
Вы не можете даже получить доступ к переменным, определённым в том же модуле, если не укажете явно контекст:
defmodule Hygiene do
defmacro write do
quote do
a = 1
end
end
defmacro read do
quote do
a
end
end
end
require Hygiene
Hygiene.write()
Hygiene.read()
** (CompileError) undefined variable "a" (context Hygiene)
Для этого вы можете явно передать текущий контекст модуля в качестве аргумента:
defmodule ContextHygiene do
defmacro write do
quote do
var!(a, ContextHygiene) = 1
end
end
defmacro read do
quote do
var!(a, ContextHygiene)
end
end
end
require Hygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1
Контексты переменной определяются третьим элементом кортежа. По умолчанию контекстом является nil, а quote назначает другой контекст всем переменным внутри:
quote(do: var)
#=> {:var, [], Elixir}
В случае переменных, возвращаемых макросами, в метаданных может также быть ключ :counter, который используется для дальнейшего уточнения контекстов и обеспечения изоляции между вызовами макросов, как показано в предыдущем примере.
Гигиена псевдонимов
Псевдонимы внутри quote по умолчанию гигиеничны. Рассмотрим следующий пример:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
Hygiene.no_interference()
#=> %{}
Обратите внимание, что, даже если псевдоним M недоступен в контексте расширения макроса, приведенный выше код работает, потому что M всё ещё расширяется до Map.
Аналогично, даже если мы определили псевдоним с тем же именем перед вызовом макроса, это не повлияет на результат макроса:
defmodule Hygiene do
alias Map, as: M
defmacro no_interference do
quote do
M.new()
end
end
end
require Hygiene
alias SomethingElse, as: M
Hygiene.no_interference()
#=> %{}
В некоторых случаях вам нужно получить доступ к псевдониму или модулю, определённому в вызывающей функции. Для этого вы можете использовать макрос alias!:
defmodule Hygiene do
# This will expand to Elixir.Nested.hello()
defmacro no_interference do
quote do
Nested.hello()
end
end
# This will expand to Nested.hello() for
# whatever is Nested in the caller
defmacro interference do
quote do
alias!(Nested).hello()
end
end
end
defmodule Parent do
defmodule Nested do
def hello, do: "world"
end
require Hygiene
Hygiene.no_interference()
** (UndefinedFunctionError) ...
Hygiene.interference()
#=> "world"
end
Гигиена импортов
Аналогично псевдонимам, импорты в Elixir гигиеничны. Рассмотрим следующий код:
defmodule Hygiene do
defmacrop get_length do
quote do
length([1, 2, 3])
end
end
def return_length do
import Kernel, except: [length: 1]
get_length
end
end
Hygiene.return_length()
#=> 3
Обратите внимание, как Hygiene.return_length/0 возвращает 3, даже если функция Kernel.length/1 не импортирована. На самом деле, даже если return_length/0 импортировал функцию с тем же именем и арностью из другого модуля, это не повлияет на результат функции:
def return_length do import String, only: [length: 1] get_length end
Вызов этого нового return_length/0 по-прежнему вернёт 3 в качестве результата.
Elixir достаточно умен, чтобы отложить разрешение до самого последнего момента. Поэтому, если вы вызываете length([1, 2, 3]) внутри quote, но функция length/1 недоступна, она расширяется в вызывающей функции:
defmodule Lazy do
defmacrop get_length do
import Kernel, except: [length: 1]
quote do
length("hello")
end
end
def return_length do
import Kernel, except: [length: 1]
import String, only: [length: 1]
get_length
end
end
Lazy.return_length()
#=> 5
Информация о трассировке стека
При определении функций с помощью макросов разработчики могут выбрать, будут ли ошибки времени выполнения сообщаться из вызывающей функции или изнутри quote. Посмотрим на пример:
# adder.ex
defmodule Adder do
@doc "Defines a function that adds two numbers"
defmacro defadd do
quote location: :keep do
def add(a, b), do: a + b
end
end
end
# sample.ex
defmodule Sample do
import Adder
defadd
end
require Sample
Sample.add(:one, :two)
** (ArithmeticError) bad argument in arithmetic expression
adder.ex:5: Sample.add/2
При использовании location: :keep и передаче неверных аргументов функции Sample.add/2, информация о трассировке стека будет указывать на файл и строку внутри quote. Без location: :keep, ошибка сообщается туда, где defadd был вызван. location: :keep влияет только на определения внутри quote.
location: :keepи unquoteНе используйте
location: :keepесли определение функции такжеunquoteаргументов макроса. В противном случае Elixir сохранит определение файла текущей позиции, но аргументы unquote могут содержать информацию о строке вызывающей функции макроса, что приведёт к неверной трассировке стека.
Привязка и фрагменты unquote
Механизмы quote/unquote Elixir предоставляют функциональность под названием фрагменты unquote. Фрагменты unquote обеспечивают лёгкий способ генерации функций на лету. Рассмотрим этот пример:
kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
В примере выше мы динамически сгенерировали функции foo/0 и bar/0. Теперь представьте, что мы хотим преобразовать эту функциональность в макрос:
defmacro defkv(kv) do
Enum.map(kv, fn {k, v} ->
quote do
def unquote(k)(), do: unquote(v)
end
end)
end
Мы можем вызвать этот макрос как:
defkv [foo: 1, bar: 2]
Однако мы не можем вызвать его следующим образом:
kv = [foo: 1, bar: 2] defkv kv
Это потому, что макрос ожидает, что его аргументы будут списком ключевых слов во время **компиляции**. Поскольку в примере выше мы передаём представление переменной kv, наш код завершается сбоем.
Это распространённая ловушка при разработке макросов. Мы предполагаем определённую форму в макросе. Мы можем обойти это, процитировав переменную внутри цитируемого выражения:
defmacro defkv(kv) do
quote do
Enum.each(unquote(kv), fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
Если вы попытаетесь запустить наш новый макрос, вы заметите, что он даже не будет компилироваться, поскольку будет жаловаться, что переменные k и v не существуют. Это связано с неоднозначностью: unquote(k) может быть фрагментом unquote, как раньше, или обычным unquote, как в unquote(kv).
Одним из решений этой проблемы является отключение unquoting в макросе, однако это сделает невозможным внедрение представления kv в дерево. Именно тогда опция :bind_quoted приходит на помощь (снова!). Используя :bind_quoted, мы можем автоматически отключить unquoting, одновременно внедряя необходимые переменные в дерево:
defmacro defkv(kv) do
quote bind_quoted: [kv: kv] do
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
end
На самом деле, опция :bind_quoted рекомендуется всякий раз, когда требуется внедрить значение в quote.
receive(args)Source
Проверяет, есть ли сообщение, соответствующее любому из заданных пунктов в текущем почтовом ящике процесса.
Если нет соответствующего сообщения, текущий процесс ожидает, пока придёт соответствующее сообщение или пока не истечёт заданное время ожидания.
Любые новые и существующие сообщения, не соответствующие условиям, останутся в почтовом ящике.
Примеры
receive do
{:selector, number, name} when is_integer(number) ->
name
name when is_atom(name) ->
name
_ ->
IO.puts(:stderr, "Unexpected message received")
end
Необязательный after пункт может быть указан в случае, если соответствующее сообщение не получено в течение заданного интервала времени, указанного в миллисекундах:
receive do
{:selector, number, name} when is_integer(number) ->
name
name when is_atom(name) ->
name
_ ->
IO.puts(:stderr, "Unexpected message received")
after
5000 ->
IO.puts(:stderr, "No message in 5 seconds")
end
Пункт after может быть указан даже если нет пунктов соответствия. Значение таймаута, заданное для after может быть любым выражением, результатом которого является одно из допустимых значений:
:infinity- процесс должен бесконечно ожидать соответствующего сообщения, это эквивалентно тому, что условие after не используется0- если в почтовом ящике нет соответствующего сообщения, таймаут произойдёт немедленноположительное целое число, меньшее или равное
4_294_967_295(0xFFFFFFFFв шестнадцатеричной записи) - значение таймаута должно быть представлено как беззнаковое 32-битное целое число.
Обработка переменных
Специальная форма receive/1 обрабатывает переменные точно так же, как и специальный макрос case/2. Для получения дополнительной информации, ознакомьтесь с документацией для case/2.
require(module, opts)Source
Требует модуль для использования его макросов.
Примеры
Публичные функции в модулях доступны глобально, но для использования макросов необходимо явным образом потребовать модуль, в котором они определены.
Предположим, вы создали собственное if/2 реализацию в модуле MyMacros. Если вы хотите вызвать его, вам сначала необходимо явно потребовать модуль MyMacros.
defmodule Math do require MyMacros MyMacros.if do_something, it_works end
Попытка вызвать макрос, который не был загружен, приведёт к ошибке.
Сокращение псевдонима
require/2 также принимает :as в качестве опции, чтобы автоматически настроить псевдоним. Для получения дополнительной информации см. alias/2.
super(args)Source
Вызывает переопределённую функцию при её переопределении с помощью Kernel.defoverridable/1.
См. Kernel.defoverridable/1 для получения дополнительной информации и документации.
try(args)Source
Вычисляет заданные выражения и обрабатывает любые ошибки, выходы или выбрасывания, которые могли произойти.
Примеры
try do
do_something_that_may_fail(some_arg)
rescue
ArgumentError ->
IO.puts("Invalid argument given")
catch
value ->
IO.puts("Caught #{inspect(value)}")
else
value ->
IO.puts("Success! The result was #{inspect(value)}")
after
IO.puts("This is printed regardless if it failed or succeeded")
end
Пункт rescue используется для обработки исключений, а пункт catch может быть использован для перехвата брошенных значений и выходов. Пункт else может использоваться для управления потоком, основываясь на результате выражения. catch, rescue, и else пункты работают на основе сопоставления с образцом (аналогично специальной форме case).
Вызовы внутри try/1 не являются хвостовой рекурсией, так как виртуальной машине необходимо сохранить стек вызовов на случай возникновения исключения. Чтобы получить стек вызовов, обратитесь к __STACKTRACE__/0 внутри пункта rescue или catch.
Пункты rescue
Помимо использования сопоставления с образцом, пункты rescue предоставляют удобные возможности для обработки исключений, позволяющие перехватить исключение по его имени. Все следующие форматы являются допустимыми шаблонами в пунктах rescue:
# Rescue a single exception without binding the exception # to a variable try do UndefinedModule.undefined_function rescue UndefinedFunctionError -> nil end # Rescue any of the given exception without binding try do UndefinedModule.undefined_function rescue [UndefinedFunctionError, ArgumentError] -> nil end # Rescue and bind the exception to the variable "x" try do UndefinedModule.undefined_function rescue x in [UndefinedFunctionError] -> nil end # Rescue all kinds of exceptions and bind the rescued exception # to the variable "x" try do UndefinedModule.undefined_function rescue x -> nil end
Ошибки Erlang
Ошибки Erlang преобразуются в ошибки Elixir при перехвате:
try do :erlang.error(:badarg) rescue ArgumentError -> :ok end #=> :ok
Наиболее распространённые ошибки Erlang преобразуются в их эквиваленты Elixir. Те, которые не преобразуются, будут преобразованы в более общую ошибку ErlangError:
try do :erlang.error(:unknown) rescue ErlangError -> :ok end #=> :ok
Фактически, ErlangError может использоваться для перехвата любых ошибок, которые не являются надлежащими ошибками Elixir. Например, она может использоваться для перехвата предыдущей ошибки :badarg до преобразования:
try do :erlang.error(:badarg) rescue ErlangError -> :ok end #=> :ok
Пункты catch
Пункт catch может использоваться для перехвата брошенных значений, выходов и ошибок.
Перехват брошенных значений
catch может использоваться для перехвата значений, брошенных функцией Kernel.throw/1:
try do
throw(:some_value)
catch
thrown_value ->
IO.puts("A value was thrown: #{inspect(thrown_value)}")
end
Перехват значений любого типа
Пункт catch также поддерживает перехват выходов и ошибок. Для этого он позволяет сопоставлять как тип перехваченного значения, так и само значение:
try do
exit(:shutdown)
catch
:exit, value ->
IO.puts("Exited with value #{inspect(value)}")
end
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts("Caught exit or throw with value #{inspect(value)}")
end
Пункт catch также поддерживает :error наряду с :exit и :throw, как и в Erlang, хотя это обычно избегают в пользу механизмов управления raise/rescue. Одна из причин этого заключается в том, что при перехвате :error, ошибка не преобразуется автоматически в ошибку Elixir:
try do :erlang.error(:badarg) catch :error, :badarg -> :ok end #=> :ok
Пункты after
Пункт after позволяет определить логику очистки, которая будет вызвана как при успешном выполнении блока кода, переданного try/1, так и при возникновении ошибки. Обратите внимание, что процесс завершит работу обычным способом при получении сигнала выхода, который приводит к его внезапному завершению, поэтому выполнение пункта after не гарантируется. К счастью, большинство ресурсов в Elixir (например, открытые файлы, таблицы ETS, порты, сокеты и т.д.) связаны с процессом или отслеживают его, и автоматически очищают себя, если этот процесс завершается.
File.write!("tmp/story.txt", "Hello, World")
try do
do_something_with("tmp/story.txt")
after
File.rm("tmp/story.txt")
end
Хотя пункты after вызываются независимо от наличия ошибки, они не изменяют возвращаемое значение. Все приведенные ниже примеры возвращают :return_me:
try do
:return_me
after
IO.puts("I will be printed")
:not_returned
end
try do
raise "boom"
rescue
_ -> :return_me
after
IO.puts("I will be printed")
:not_returned
end
Пункты else
Пункты else позволяют сопоставить с образцом результат тела, переданного try/1:
x = 2
try do
1 / x
rescue
ArithmeticError ->
:infinity
else
y when y < 1 and y > -1 ->
:small
_ ->
:large
end
Если пункт else отсутствует, а исключения не возникают, возвращается результат выражения:
x = 1
^x =
try do
1 / x
rescue
ArithmeticError ->
:infinity
end
Однако, если пункт else присутствует, но результат выражения не соответствует ни одному из шаблонов, будет поднято исключение. Это исключение не будет перехвачено пунктом catch или rescue в том же try:
x = 1
try do
try do
1 / x
rescue
# The TryClauseError cannot be rescued here:
TryClauseError ->
:error_a
else
0 ->
:small
end
rescue
# The TryClauseError is rescued here:
TryClauseError ->
:error_b
end
Аналогично, исключение внутри пункта else не перехватывается и не обрабатывается внутри того же try:
try do
try do
nil
catch
# The exit(1) call below can not be caught here:
:exit, _ ->
:exit_a
else
_ ->
exit(1)
end
catch
# The exit is caught here:
:exit, _ ->
:exit_b
end
Это означает, что виртуальная машина больше не нуждается в сохранении стека вызовов после попадания в пункт else и, таким образом, возможна хвостовая рекурсия при использовании try с вызовом на хвосте как последнего вызова внутри пункта else. То же самое относится к пунктам rescue и catch.
Только результат обработанного выражения передаётся в пункт else . Если обработанное выражение попадает в пункты rescue или catch, их результат не будет передан в пункт else:
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end
Обработка переменных
Поскольку выражение внутри try может не быть вычислено из-за исключения, любая переменная, созданная внутри try, не может быть обращена извне. Например:
try do x = 1 do_something_that_may_fail(same_arg) :ok catch _, _ -> :failed end x #=> unbound variable "x"
В приведённом выше примере к переменной x нет доступа, так как она была определена внутри пункта try. Общим подходом к решению этой проблемы является возврат переменных, определённых внутри try:
x =
try do
x = 1
do_something_that_may_fail(same_arg)
x
catch
_, _ -> :failed
end unquote(expr)Source
Раскладывает данное выражение внутри цитируемого выражения.
Эта функция ожидает допустимое выражение AST Elixir, также известное как цитируемое выражение, в качестве аргумента. Если вы хотите unquote любое значение, такое как карта или кортеж из четырёх элементов, вы должны вызвать Macro.escape/1 перед раскладыванием.
Примеры
Представьте ситуацию, когда у вас есть цитируемое выражение, и вы хотите вставить его внутрь другого цитируемого выражения. Первая попытка будет такой:
value =
quote do
13
end
quote do
sum(1, value, 3)
end
В результате аргумент для вызова функции :sum не будет ожидаемым:
{:sum, [], [1, {:value, [], Elixir}, 3]}
Для этого мы используем unquote:
iex> value =
...> quote do
...> 13
...> end
iex> quote do
...> sum(1, unquote(value), 3)
...> end
{:sum, [], [1, 13, 3]}
Если вы хотите раскладывать значение, которое не является цитируемым выражением, например, карту, вам нужно вызвать Macro.escape/1 перед этим:
iex> value = %{foo: :bar}
iex> quote do
...> process_map(unquote(Macro.escape(value)))
...> end
{:process_map, [], [{:%{}, [], [foo: :bar]}]}
Если вы забудете это сделать, Elixir выведет ошибку при компиляции кода.
unquote_splicing(expr)Source
Раскладывает данный список, расширяя его аргументы.
Аналогично unquote/1.
Примеры
iex> values = [2, 3, 4]
iex> quote do
...> sum(1, unquote_splicing(values), 5)
...> end
{:sum, [], [1, 2, 3, 4, 5]} with(args)Source
Комбинирует сопоставляющие клаузы.
Один из способов понять with — показать, какие шаблоны кода он улучшает. Представьте, что у вас есть карта, где поля width и height необязательны, и вы хотите вычислить её площадь, как {:ok, area} или вернуть :error. Мы могли бы реализовать эту функцию как:
def area(opts) do
case Map.fetch(opts, :width) do
{:ok, width} ->
case Map.fetch(opts, :height) do
{:ok, height} -> {:ok, width * height}
:error -> :error
end
:error ->
:error
end
end
при вызове как area(%{width: 10, height: 15}), она должна вернуть {:ok, 150}. Если любое из полей отсутствует, она возвращает :error.
Хотя приведенный выше код работает, он довольно громоздкий. Используя with, мы можем переписать его как:
def area(opts) do
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height) do
{:ok, width * height}
end
end
Вместо определения вложенных case с клаузами, мы используем with вместе с оператором PATTERN <- EXPRESSION для сопоставления выражений справа от него с шаблоном слева. Рассматривайте <- как брата =, за исключением того, что, в то время как = поднимает исключение в случае отсутствия соответствия, <- просто прерывает цепочку with и возвращает несопоставленное значение.
Давайте попробуем это в IEx:
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
{:ok, 150}
Если все клаузы соответствуют, выполняется блок do, возвращая его результат. В противном случае цепочка прерывается, и возвращается несопоставленное значение:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> end
:error
Гварды могут использоваться в шаблонах:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
...> {:ok, to_string(role)}
...> end
{:ok, "admin"}
Как и в for/1, переменные, привязанные внутри with/1, будут недоступны вне with/1.
Выражения без <- также могут использоваться в клаузах. Например, вы можете выполнить стандартное сопоставление с оператором =:
iex> width = nil
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, double_width * height}
...> end
{:ok, 300}
iex> width
nil
Поведение любого выражения в клаузе такое же, как если бы оно было написано вне with. Например, = вызовет исключение MatchError вместо возвращения несопоставленного значения:
with :foo = :bar, do: :ok ** (MatchError) no match of right hand side value: :bar
Как и при любом другом вызове функции или макроса в Elixir, явные скобки также могут быть использованы вокруг аргументов перед блоком do-end:
iex> opts = %{width: 10, height: 15}
iex> with(
...> {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height)
...> ) do
...> {:ok, width * height}
...> end
{:ok, 150}
Выбор между скобками и их отсутствием зависит от предпочтений.
Клаузы else
Опция else может быть задана для изменения того, что возвращает with в случае неудачного сопоставления:
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height) do
...> {:ok, width * height}
...> else
...> :error ->
...> {:error, :wrong_data}
...>
...> _other_error ->
...> :unexpected_error
...> end
{:error, :wrong_data}
Блок else работает как клауза case: он может иметь несколько клауз, и будет использована первая совпавшая. Переменные, привязанные внутри with (например, width в этом примере), недоступны в блоке else.
Если используется блок else и нет совпадающих клауз, генерируется исключение WithClauseError.
Обратите внимание!
Помните, что одним из потенциальных недостатков with является то, что все клаузы ошибки сглаживаются в один блок else. Например, рассмотрим этот код, который проверяет, указывает ли заданный путь на файл Elixir и существует ли он перед созданием резервной копии:
with ".ex" <- Path.extname(path),
true <- File.exists?(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
else
binary when is_binary(binary) ->
{:error, :invalid_extension}
false ->
{:error, :missing_file}
end
Обратите внимание, как нам приходится перестраивать типы результатов Path.extname/1 и File.exists?/1 для создания сообщений об ошибках. В этом случае лучше переработать код так, чтобы каждая <- уже возвращала желаемый формат в случае ошибок, как в этом примере:
with :ok <- validate_extension(path),
:ok <- validate_exists(path) do
backup_path = path <> ".backup"
File.cp!(path, backup_path)
{:ok, backup_path}
end
defp validate_extension(path) do
if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
end
defp validate_exists(path) do
if File.exists?(path), do: :ok, else: {:error, :missing_file}
end
Обратите внимание, как код выше организован лучше и понятнее, как только мы убеждаемся, что каждая <- в with возвращает нормализованный формат.
^varSource
Оператор фиксации. Доступ к уже привязанной переменной в клаузах сопоставления.
Примеры
Elixir позволяет перепривязывать переменные с помощью статического единичного назначения:
iex> x = 1 iex> x = x + 1 iex> x 2
Однако в некоторых ситуациях полезно сопоставлять с существующим значением вместо перепривязки. Это можно сделать с помощью специальной формы ^, обычно называемой оператором фиксации:
iex> x = 1 iex> ^x = List.first([1]) iex> ^x = List.first([2]) ** (MatchError) no match of right hand side value: 2
Обратите внимание, что ^x всегда ссылается на значение x до сопоставления. Следующий пример будет соответствовать:
iex> x = 0
iex> {x, ^x} = {1, 0}
iex> x
1 {args}Source
Создаёт кортеж.
Дополнительную информацию о типе данных кортежей и функциях для работы с кортежами можно найти в модуле Tuple; некоторые функции для работы с кортежами также доступны в Kernel (такие как Kernel.elem/2 или Kernel.tuple_size/1).
Представление AST
Только кортежи из двух элементов считаются литералами в Elixir и возвращают себя при цитировании. Поэтому все остальные кортежи представлены в AST как вызовы специальной формы :{}.
iex> quote do
...> {1, 2}
...> end
{1, 2}
iex> quote do
...> {1, 2, 3}
...> end
{:{}, [], [1, 2, 3]}
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/Kernel.SpecialForms.html