Демистификация одной из самых мощных функций Rust.

Одна из самых замечательных и мощных возможностей Rust - это возможность использовать и создавать макросы. К сожалению, синтаксис для создания макросов может быть довольно пугающим, а примеры могут быть ошеломляющими для начинающих разработчиков.

Я обещаю вам, что макросы Rust действительно просты для понимания. Эта статья даст вам представление о том, как создавать свои собственные макросы.

Что такое макросы в Rust? 😮

Если вы уже пробовали Rust, вам уже следовало использовать макрос раньше, println!. Этот макрос позволяет печатать строку текста с возможностью интерполировать переменные в текстовой строке.

Макросы просто позволяют вам изобрести собственный синтаксис и написать код, который записывает больше кода.

Это называется метапрограммированием, , которое позволяет использовать синтаксические сахара, которые делают ваш код короче и упрощают использование ваших библиотек. Вы даже можете создать свой собственный DSL (предметно-ориентированный язык) в ржавчине.

Макросы работают путем сопоставления с конкретными шаблонами, определенными в макро-правиле, захватывая часть совпадения как переменные, а затем расширяя , чтобы создать еще больше кода .

Ничего страшного, если ты чего-то не понимаешь. Давайте просто погрузимся прямо сейчас!

Как создать макрос? 😯

Мы можем создавать макросы с помощью макроса macro_rules!. Макроцепция!

Вот как можно создать пустой hey! макрос; Очевидно, на данный момент он ничего не делает.

Эта () => {} часть кажется интригующей, не правда ли?

Это запись для правила макроса, которому можно сопоставить множество правил в одном макросе. Это очень похоже на сопоставление с образцом, где также может быть много случаев, разделенных запятыми.

Но что это на самом деле означает?

Часть скобок - это средство сопоставления, которое позволяет нам сопоставлять шаблоны и фиксировать их часть как переменные. . Таким образом мы можем изобрести собственные собственные синтаксисы и DSL.

Часть фигурных скобок - это преобразователь, где мы можем использовать переменные, полученные из сопоставителя. Компилятор Rust расширит код нашего макроса и его переменные до реального кода Rust.

Сопоставление и захват шаблонов ✏️

Но как мы на самом деле подходим к шаблону? Давайте посмотрим.

Rust будет пытаться сопоставить шаблоны, определенные в парах сопоставления, которые могут быть (), {} или []. Символы в правилах макросов будут сопоставлены с вводом, чтобы определить, совпадает он или нет.

После успешного совпадения шаблона правила мы можем захватить части шаблона и записать их как переменную для использования в транскрибере. Вот как это сделать.

Первая часть после знака доллара - это имя переменной, которое будет доступно как переменная в транскрибере, где находится наш код. В этом примере мы записываем его как $ name.

Последняя часть после точки с запятой называется указателем. Это типы, которые мы можем выбрать для сопоставления. Например, в настоящее время мы используем обозначение выражения, обозначенное знаком expr после двоеточия.

Это говорит Rust, что нужно сопоставить выражение и записать его как $name.

Есть много обозначений, которые мы могли бы использовать, а не только выражения. Вот краткий список обозначений, доступных в Rust:

  • item: элемент, такой как функция, структура, модуль и т. Д.
  • block: блок (т.е. блок инструкций и / или выражения, заключенный в фигурные скобки)
  • stmt: заявление
  • pat: узор
  • expr: выражение
  • ty: тип
  • ident: идентификатор
  • path: путь (например, foo, ::std::mem::replace, transmute::<_, int>, ...)
  • meta: мета-элемент; вещи, которые входят в атрибуты #[...] и #![...]
  • tt: единое дерево токенов

Теперь, как нам использовать эти захваченные переменные в транскрайбере?

Достаточно просто. Мы просто используем те захваченные переменные, которые мы захватили в сопоставлении, перед которым стоит знак доллара. Он работает как обычная переменная; здесь ничего особенного.

Наш первый макрос! 🌈

Прохладный! Теперь мы знаем достаточно информации, чтобы создать простой макрос. Давайте создадим наш собственный макрос yo!.

Вот и все, мы только что создали наш первый макрос! Эта программа распечатает Yo, Finn! в результате вызова макроса.

Мы сопоставили входное выражение и записали его как переменную $ name. Затем мы используем захваченное $ name в транскрибере, которое будет расширено компилятором Rust.

Это кажется простым, не правда ли?

Повторения, повторения, повторения, повторения, повторения, повторения, повторения, повторения…

Многие из макросов, которые мы знаем и любим, могут требовать большого количества входных данных одновременно. В качестве примера возьмем макрос vec!; мы можем создать вектор с элементами в нем, вызвав макрос vec! следующим образом: vec![1, 2, 3, 4, 5].

Как vec! это делает? Конечно, он не собирается собирать пару тысяч переменных и вводить их вручную одну за другой, верно? Это секрет:

Мы просто помещаем шаблоны, которые хотим повторить, внутрь $(...) части. Затем вставьте разделитель, в данном случае это символ запятая (,). Это будет символ, который будет разделять узоры, позволяя нам иметь повторения.

Наконец, мы добавляем в конец символ звездочки (*), который будет многократно совпадать с шаблоном внутри $(), пока не закончатся совпадения.

Смущенный? Посмотрим на пример!

В этом случае для макроса hey! мы фиксируем все входные выражения как $ name и будем продолжать сопоставление его до тех пор, пока совпадения не закончатся. Мы можем вызывать этот макрос с любым количеством аргументов.

Все еще не понимаете? Совершенно нормально! Давайте реализуем отличный макрос из реального мира, чтобы увидеть, как это работает.

Реализация синтаксиса Ruby HashMap в Rust

Если вы когда-либо раньше программировали на Ruby, то наверняка знаете его потрясающий синтаксис хеширования, key => value. Я бы хотел иметь такой же синтаксис для создания HashMaps в Rust!

К счастью, в нашем распоряжении есть макросы, и мы можем изобрести собственный синтаксис и воплотить мечты в жизнь всего несколькими строками кода. (спойлеры: семь)

Начнем с простого. Мы можем использовать шаблон $key:expr => $value:expr для захвата выражений $ key и $ value, разделенных использованием =>.

Это уже хорошо работает, но позволит нам сопоставлять только одну key => value пару, что не очень практично для HashMaps, которые часто имеют более одной пары ключ-значение. Здесь пригодится звездочка (*).

Помещая наш узор в $(),*, мы разрешаем повторение этого шаблона. Это означает, что у нас может быть столько key => value пар, сколько мы захотим. Ура!

Когда макрос вызывается, захваченная переменная будет назначаться для каждого повторения.

Но как мы собираемся использовать эти повторяющиеся захваченные переменные? Все становится ясно, когда мы используем их в транскрайбере.

В транскрибере мы можем заметить использование $()* в нашем коде. Это означает, что этот код будет расширен специально для каждого повторяющегося слоя!

Это расширится до hm.insert() $key и $value до HashMap для каждого повторяющегося слоя. Давайте представим это немного.

Как видите, когда мы вызываем map!("name" => "Finn", "gender" => "Boy"), мы создаем два повторяющихся слоя.

Пары key => value будут преобразованы в код, указанный в части транскрибера, который был указан как hm.insert($key, $value), где $key и $value являются захваченными переменными.

Уф, это было довольно много! Давайте соберем все вместе и создадим наш собственный макрос map!.

Всего за семь строк кода мы создали полнофункциональный макрос! Давайте создадим программу и попробуем ее вызвать. Посмотрите, работает ли это.

Результатом этой программы будет User {“gender”: “Boy”, “name”: “Finn”}. Вот и сработало! Вот как мы можем создавать макросы в Rust.

Если вы хотите узнать больше о том, как разрабатывать макросы для реального использования в Rust, ознакомьтесь с этими ресурсами:

На этом пока все! 💖

Спасибо всем, что дожили до конца! Я надеюсь, что у вас наконец-то хватит смелости создать свои собственные макросы и реализовать свои желания метапрограммирования в Rust.

Увидимся в следующей статье! ✨