- PVSM.RU - https://www.pvsm.ru -

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

Процедурные макросы в Rust 1.15 - 1Ребята, свершилось! После долгих шести недель ожидания наконец вышла версия Rust 1.15 [1] с блекджеком и процедурными макросами.

По моему нескромному мнению, это самый значительный релиз, после эпического 1.0 [2]. Среди множества вкусных вещей в этом релизе были стабилизированы процедурные макросы [3], взрывающие мозг [4] своим могуществом, удобством и безопасностью.

А что же это дает простым смертным? Практически бесплатную [де]сериализацию [5], удобный интерфейс к БД [6], интуитивный веб фреймворк [7], выводимые конструкторы [8] и много чего еще.

Да, если вы все еще не добрались до этого языка, то сейчас самое время попробовать, тем более, что теперь установить компилятор и окружение [9] можно одной командой:

curl https://sh.rustup.rs -sSf | sh

Впрочем, обо всем по порядку.

Немного истории

Долгое время автоматически выводить можно было только стандартные типажи, такие как Eq [10], PartialEq [11], Ord [12], PartialOrd [13], Debug [14], Copy [15], Clone [16]. Теперь это возможно и для пользовательских типов.

Вместо ручной реализации достаточно написать #[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, не может вывести все методы самостоятельно. Однако, в некоторых случаях хочется иметь возможность подсказать компилятору, как следует выводить те или иные методы и для нестандартных структур, а дальше предоставить ему полную свободу действий. Такой подход может применяться в довольно сложных механизмах, избавляя программиста от необходимости писать код руками.

Процедурные макросы [3] позволяют добавить в язык элемент метапрограммирования и тем самым существенно упростить рутинные операции, такие как сериализация или обработка запросов.

Ну хорошо, скажете вы, это все чудесно, а где же примеры?

Сериализация

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

А если нет? Если данные представляют собой набор сложных структур со строками произвольной длины, массивами, хеш таблицами и B-деревьями? Так или иначе, такие данные придется сериализовать.

Конечно, в истории Computer Science такая задача возникала неоднократно и ответ обычно кроется в библиотеках сериализации, навроде Google Protobuf [17].

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

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

// Подключаем библиотеку и макро-определения
#[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 и другие. Из коробки поддерживается сериализация и десериализация всех контейнеров из стандартной библиотеки [19] Rust.

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

Чтение конфигурации

Кстати о десериализации. Выше мы увидели, как можно взять JSON строку и получить из нее структуру с заполненными полями. Тот же подход можно применить и для чтения файлов конфигурации.

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

Работа с БД

Разумеется, одной сериализацией дело не ограничивается. Например, библиотека Diesel [6] предоставляет удобный интерфейс к базам данных, который тоже стал возможен благодаря процедурным макросам и автоматическому выводу методов в 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);
    }
}

Полный пример [20] можно найти на сайте библиотеки.

А что там с вебом?

Может быть, мы хотим обработать пользовательский запрос? И снова возможности языка позволяют писать интуитивный код, который «просто работает».

Ниже приведен пример кода с использованием фреймворка Rocket [21] который реализует простейший счетчик:

Смотреть

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 работают очень неплохо. А если вспомнить, что сам язык является безопасным в отношении памяти и позволяет писать безопасный многопоточный код [22], свободный от состояния гонок, то все становится совсем хорошо.

Очень радует, что теперь эти возможности доступны и в стабильной версии языка, ведь многие жаловались, что ночные сборки приходилось использовать только из-за Serde и Diesel. Теперь такой проблемы нет.

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

Автор: Halt

Источник [23]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/242046

Ссылки в тексте:

[1] вышла версия Rust 1.15: https://blog.rust-lang.org/2017/02/02/Rust-1.15.html

[2] эпического 1.0: https://blog.rust-lang.org/2015/05/15/Rust-1.0.html

[3] процедурные макросы: https://doc.rust-lang.org/book/procedural-macros.html

[4] мозг: http://www.braintools.ru

[5] бесплатную [де]сериализацию: https://serde.rs/

[6] интерфейс к БД: http://diesel.rs/

[7] веб фреймворк: https://rocket.rs/

[8] выводимые конструкторы: https://github.com/nrc/derive-new

[9] установить компилятор и окружение: https://www.rust-lang.org/en-US/install.html

[10] Eq: https://doc.rust-lang.org/std/cmp/trait.Eq.html

[11] PartialEq: https://doc.rust-lang.org/std/cmp/trait.PartialEq.html

[12] Ord: https://doc.rust-lang.org/std/cmp/trait.Ord.html

[13] PartialOrd: https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html

[14] Debug: https://doc.rust-lang.org/std/fmt/trait.Debug.html

[15] Copy: https://doc.rust-lang.org/std/marker/trait.Copy.html

[16] Clone: https://doc.rust-lang.org/std/clone/trait.Clone.html

[17] Google Protobuf: https://github.com/google/protobuf

[18] библиотека для сериализации: http://serde.rs

[19] стандартной библиотеки: https://doc.rust-lang.org/std/

[20] Полный пример: http://diesel.rs/guides/getting-started/

[21] фреймворка Rocket: https://rocket.rs/news/2017-02-06-version-0.2/

[22] безопасный многопоточный код: https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html

[23] Источник: https://habrahabr.ru/post/321564/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best