Процедурные макросы в Rust 1.15

в 15:10, , рубрики: diesel, rocket, Rust, serde, Компиляторы, Программирование, процедурные макросы, Разработка веб-сайтов, системное программирование, счастье есть

Процедурные макросы в Rust 1.15 - 1Ребята, свершилось! После долгих шести недель ожидания наконец вышла версия 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. Теперь такой проблемы нет.

В следующей статье я расскажу о том, как же все-таки писать эти макросы и что еще можно делать с их помощью.

Автор: Halt

Источник

Поделиться

* - обязательные к заполнению поля