Процедурные макросы в Rust 1.15
Ребята, свершилось! После долгих шести недель ожидания наконец вышла версия Rust 1.15 с блекджеком и процедурными макросами.
По моему нескромному мнению, это самый значительный релиз, после эпического 1.0. Среди множества вкусных вещей в этом релизе были стабилизированы процедурные макросы, взрывающие мозг своим могуществом, удобством и безопасностью.
А что же это дает простым смертным? Практически бесплатную [де]сериализацию, удобный интерфейс к БД, интуитивный веб фреймворк, выводимые конструкторы и много чего еще.
Да, если вы все еще не добрались до этого языка, то сейчас самое время попробовать, тем более, что теперь установить компилятор и окружение стало можно одной строкой:
curl https://sh.rustup.rs -sSf | sh
Впрочем, обо всем по порядку.
Немного истории
Долгое время автоматически выводить можно было только стандартные маркеры и типажи, такие как Eq, PartialEq, Debug, Copy, Clone.
Вместо ручной реализации достаточно было написать #[derive(имя_типажа)]
, а остальное компилятор делал за нас:
#[derive(Eq, Debug)]
struct Point {
x: i32,
y: i32,
}
Программистам, работавшим с Haskell, все это должно быть очень знакомо (включая названия), да и применяется оно примерно в тех же случаях. Компилятор, обнаружив атрибут derive
, пройдется по списку типажей и реализует для них стандартный набор методов в меру своего понимания.
Например, для типажа Eq
будет реализован метод fn eq(&self, other: &Point) -> bool
путем последовательного сравнения полей структуры. Таким образом, структуры будут считаться равными, если равны их поля.
Конечно, в тех случаях, когда желаемое поведение отличается от поведения по умолчанию, программист может определить реализацию типажа собственноручно, например так:
use std::fmt;
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "My cool point with x: {} and y: {}", self.x, self.y)
}
}
Как бы там ни было, автоматический вывод типажей заметно упрощает кодирование и делает текст программы более читаемым и лаконичным.
Процедурные макросы
Компилятор, даже такой умный как Rust, не может вывести все методы самостоятельно. Однако, в некоторых случаях хочется иметь возможность подсказать компилятору, как следует выводить те или иные методы и для нестандартных структур, а дальше предоставить ему полную свободу действий. Такой подход может применяться в довольно сложных механизмах, избавляя программиста от необходимости писать код руками.
Процедурные макросы позволяют добавить в язык элемент метапрограммирования и тем самым существенно упростить рутинные операции, такие как сериализация или обработка запросов.
Ну хорошо, скажете вы, это все чудесно, а где же примеры?
Сериализация
Часто возникает задача передачи данных в другой процесс, отправки их по сети или же записи на диск. Хорошо, если структура данных простая и легко может быть представлена в виде последовательности байтов.
А если нет? Если данные представляют собой набор сложных структур со строками произвольной длины, массивами, хеш таблицами и B-деревьями? Так или иначе, такие данные придется сериализовать.
Конечно, в истории Computer Science такая задача возникала неоднократно и ответ обычно кроется в библиотеках сериализации, навроде Google Protobuf.
Традиционно программист строит метаописание данных и протоколов на специальном декларативном языке, а затем все это дело компилируется в код, который уже используется в бизнес-логике.
В этом смысле Rust не является исключением, и библиотека для сериализации действительно есть. Вот только никакого метаописания писать не нужно. Все реализуется средствами самого языка и механизма процедурных макросов:
// Подключаем библиотеку и макро-определения
#[macro_use]
extern crate serde_derive;
// Подключаем поддержку JSON
extern crate serde_json;
// Наша структура
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// Создаем экземпляр структуры
let point = Point { x: 1, y: 2 };
// Конвертируем экземпляр в строку JSON
let serialized = serde_json::to_string(&point).unwrap();
// На печать будет выведено: serialized = {"x":1,"y":2}
println!("serialized = {}", serialized);
// Конвертируем строку JSON обратно в экземпляр Point
let deserialized: Point = serde_json::from_str(&serialized).unwrap();
// Ожидаемо, результат будет: deserialized = Point { x: 1, y: 2 }
println!("deserialized = {:?}", deserialized);
}
Помимо JSON библиотека Serde поддерживает еще массу форматов: URL, XML, Redis, YAML, MessagePack, Pickle и другие. Из коробки поддерживается сериализация и десериализация всех контейнеров из стандартной библиотеки Rust.
Похоже на магию, только это не магия. Все работает благодаря своеобразной интроспекции на этапе компиляции. А значит все ошибки будут своевременно выловлены и исправлены.
Чтение конфигурации
Кстати о десериализации. Выше мы увидели, как можно взять JSON строку и получить из нее структуру с заполненными полями. Тот же подход можно применить и для чтения файлов конфигурации.
Достаточно создать файл в одном из поддерживаемых форматов и просто десериализовать его в структуру конфигурации вместо унылого парсинга и разбора параметров по одному.
Работа с БД
Разумеется, одной сериализацией дело не ограничивается. Например, библиотека Diesel предоставляет удобный интерфейс к базам данных, который тоже стал возможен благодаря процедурным макросам и автоматическому выводу методов в Rust:
// ...
#[derive(Queryable)]
pub struct Post {
pub id: i32,
pub title: String,
pub body: String,
pub published: bool,
}
// ...
fn main() {
let connection = establish_connection();
let results = posts.filter(published.eq(true))
.limit(5)
.load::(&connection)
.expect("Error loading posts");
println!("Displaying {} posts", results.len());
for post in results {
println!("{}", post.title);
println!("----------\n");
println!("{}", post.body);
}
}
Полный пример можно найти на сайте библиотеки.
А что там с вебом?
Может быть, мы хотим обработать пользовательский запрос? И снова возможности языка позволяют писать интуитивный код, который «просто работает».
Ниже приведен пример кода с использованием фреймворка Rocket который реализует простейший счетчик:
struct HitCount(AtomicUsize);
#[get("/")]
fn index(hit_count: State) -> &'static str {
hit_count.0.fetch_add(1, Ordering::Relaxed);
"Your visit has been recorded!"
}
#[get("/count")]
fn count(hit_count: State) -> String {
hit_count.0.load(Ordering::Relaxed).to_string()
}
fn main() {
rocket::ignite()
.mount("/", routes![index, count])
.manage(HitCount(AtomicUsize::new(0)))
.launch()
}
Или, может быть, надо обработать данные из формы?
#[derive(FromForm)]
struct Task {
complete: bool,
description: String,
}
#[post("/todo", data = "")]
fn new(task: Form) -> String { ... }
Выводы
В общем и целом становится понятно, что механизмы метапрограммирования в Rust работают очень неплохо. А если вспомнить, что сам язык является безопасным в отношении памяти и позволяет писать безопасный многопоточный код, свободный от состояния гонок, то все становится совсем хорошо.
Очень радует, что теперь эти возможности доступны и в стабильной версии языка, ведь многие жаловались, что ночные сборки приходилось использовать только из-за Serde и Diesel. Теперь такой проблемы нет.
В следующей статье я расскажу о том, как же все-таки писать эти макросы и что еще можно делать с их помощью.