Процедурні макроси в 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: {} і 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, Росол та інші. З коробки підтримується сериализация і десериализация всіх контейнерів з стандартної бібліотеки 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. Тепер такої проблеми немає.
У наступній статті я розповім про те, як же все-таки писати ці макроси і що ще можна робити з їх допомогою.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.