Ребята, свершилось! После долгих шести недель ожидания наконец вышла версия Rust 1.15 с блекджеком и процедурными макросами.
По моему нескромному мнению, это самый значительный релиз, после эпического 1.0. Среди множества вкусных вещей в этом релизе были стабилизированы процедурные макросы, взрывающие мозг своим могуществом, удобством и безопасностью.
А что же это дает простым смертным? Практически бесплатную [де]сериализацию, удобный интерфейс к БД, интуитивный веб фреймворк, выводимые конструкторы и много чего еще.
Да, если вы все еще не добрались до этого языка, то сейчас самое время попробовать, тем более, что теперь установить компилятор и окружение можно одной командой:
curl https://sh.rustup.rs -sSf | sh
Впрочем, обо всем по порядку.
Немного истории
Долгое время автоматически выводить можно было только стандартные типажи, такие как Eq, PartialEq, Ord, PartialOrd, 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::<Post>(&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<HitCount>) -> &'static str { hit_count.0.fetch_add(1, Ordering::Relaxed); "Your visit has been recorded!" } #[get("/count")] fn count(hit_count: State<HitCount>) -> 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 = "<task>")] fn new(task: Form<Task>) -> String { ... }
Выводы
В общем и целом становится понятно, что механизмы метапрограммирования в Rust работают очень неплохо. А если вспомнить, что сам язык является безопасным в отношении памяти и позволяет писать безопасный многопоточный код, свободный от состояния гонок, то все становится совсем хорошо.
Очень радует, что теперь эти возможности доступны и в стабильной версии языка, ведь многие жаловались, что ночные сборки приходилось использовать только из-за Serde и Diesel. Теперь такой проблемы нет.
В следующей статье я расскажу о том, как же все-таки писать эти макросы и что еще можно делать с их помощью.