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


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

Поделиться с друзьями
-->

Комментарии (27)


  1. greabock
    10.02.2017 20:11

    мда… оказали вы услугу rocket.rs, разместив ссылку на Хабре. В пятницу. Вечером.


    1. snuk182
      10.02.2017 23:46
      +1

      Пусть привыкают. Фреймворк очень годный.


  1. apro
    10.02.2017 20:44
    +7

    Было бы неплохо добавить в статью и описание того как эти процедурные макросы работают,
    а не только как их использовать.


    1. Halt
      10.02.2017 20:53
      +5

      В следующей статье я как раз планирую это описать.


      1. Vanger13
        11.02.2017 14:53

        Ждем-с :)
        Забавно что про сам термин «Procedural macro» в вики написано одно предложение, а все остальные ссылки связаны с Rust'ом (например http://sfackler.github.io/syntax-ext-talk/#/). Неужели там все так просто что нечего и читать про эти макросы?


        1. Halt
          11.02.2017 15:03

          Макросы такого вида появились сильно раньше, чем Rust. Почитайте, например, эту статью.


          1. Vanger13
            12.02.2017 01:11

            Спасибо, ознакомился :) Про макросы знаю мало ибо не пользовался особо.
            Однако остался вопрос — как связаны понятия Procedural и hygienic macro? Я догадываюсь что в Rust макросы поддерживают обе парадигмы… но как из одного следует другое (и следует ли) я не понимаю. Мне не хватает каких-то базовых знаний на которые можно опираться. Как имплементируются системы с макросами, почему и какие бывают альтернативы.

            Кстати, не знаете ли про Scala — как называется тип макросов который используется сейчас (compile time reflection) и который придет на смену (scala-meta)? Если я правильно понял, то сейчас там как раз тупая лексическая замена, а будет что-то похожее на Scheme\Rust — с разбором AST. Прав ли я?


            1. Halt
              12.02.2017 08:51
              +2

              Процедурными их назвали в Rust чтобы подчеркнуть, что код в точке подстановки макроса генерируется процедурным образом и замена может быть сколь угодно сложной: фактически, процедура работает с куском AST и может делать с ним что угодно.

              Обычные макросы в Rust работают по принципу Сишных #define: «подставь то-то вместо того-то», с поправкой на гигиеничность и механизм сопоставления с образцом, конечно.

              Про Scala, к сожалению, ничего сказать не могу, но могу предположить, что scala-meta — это как раз аналог обычных макросов Rust, тогда как compile time reflection — аналог процедурных.


              1. senia
                12.02.2017 09:42
                +1

                Про Scala, к сожалению, ничего сказать не могу, но могу предположить, что scala-meta — это как раз аналог обычных макросов Rust, тогда как compile time reflection — аналог процедурных.
                О как же вы заблуждаетесь!
                Макросы в scala обладают всей информацией, которой обладает компилятор (ну почти).
                Если процедурные макросы в Rust оперируют набором токенов, то макросы в scala работают с AST.
                Например ниже вы мне отвечали про сериализацию полей с типами из сторонних библиотек. В scala библиотеки сериализации могут сгенерировать тайпкласс (аналог trait в Rust) для класса и всех его полей рекурсивно потому, что обладают полной информацией о типе, включая полную информацию о типе его полей. В упомянутой ссылке на библиотеке вообще тайпклассы генерируются в месте вызова методов и не надо засорять определение типа информацией о маршалинге в json (это не его ответственность).
                meta — некоторое упрощение макросов, но принцип все еще тот же.


                1. Halt
                  12.02.2017 10:03

                  Спасибо за информацию, теперь я знаю о Scala чуть больше, а не только то, что «оно есть» :)


        1. Halt
          14.02.2017 20:45
          +3

          Собственно, вторая статья: https://habrahabr.ru/post/321620/


  1. pftbest
    10.02.2017 21:03
    +5

    А еще serde умеет десериализовывать переменные окружения прямо в структуру, очень удобно.


  1. tgz
    10.02.2017 21:27
    +1

    Очень неплохо.


  1. senia
    11.02.2017 08:39
    +1

    Я правильно понимаю, что если в моей структуре есть поле типа, определенного в библиотеке, и в библиотеке не озаботились совместимостью с данным типажом сериализации, то ни какой сериализации для своей структуры я не получу?


    1. Halt
      11.02.2017 09:07
      +4

      С учетом coherence rule действительно нельзя просто взять и сериализовать внешний тип, который этим не озаботился сам. Но есть способ преодолеть это для своей библиотеки с помощью введения производного типа.

      Мне пришлось делать это для BitVec и в принципе, особых неудобств это не доставляет, поскольку deref coercions дают возможность использовать производный тип вместо основного.

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

      P.S.: Этот момент языка все еще является активно обсуждаемым, так что в будущем могут появиться и другие варианты решения.


      1. senia
        11.02.2017 20:42
        +2

        Надеюсь они придут к какому-то более удобному способу.
        Я с этой проблемой столкнулся при написании генераторов для QuickCheck. Добавление оберток помогает, но выглядит грязно.

        Вообще, предоставлять средства сериализации для библиотечных типов это такой же хороший тон, как определять Debug и Display. Я думаю, в будущем это будет повсеместно.
        Это не выход. В той же scala, где генерация typeclass макросами используется давно и продуктивно, есть несколько различных библиотек для маршалинга в json. Думаю и в Rust появятся разные библиотеки для сходных задач — у всех свои фломастеры. К которой предоставлять имплементации? Ко всем?


        1. Halt
          12.02.2017 09:14
          +5

          Тут надо понимать, что проблема далеко не тривиальна и ограничения возникают неспроста.

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

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

          Такое может возникнуть, если есть две библиотеки A и B, каждая из которых реализует у себя impl Trait for Struct и все это импортируется в библиотеку C, которая уже используется где-то у вас в коде.

          До тех пор, пока единственной точкой реализации типажа была библиотека A, все было прекрасно. Но потом разработчики библиотеки B сделали минорное изменение и добавили, например, поддержку сериализации структур (потому что их давно об этом просили).

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

          Разработчики Rust называют эту проблему «hashtable problem», потому что это еще лучше иллюстрирует проблему. Представьте, что каждая из библиотек A и B предоставила свою реализацию функции хеширования для некоего типа T. А потом заполненная хеш таблица из одной библиотеки передается в другую. Думаю не надо объяснять, к чему это может привести.

          Брать реализацию наугад, как это делает C++ в случае Weak символов — очень опасно и провоцирует сложновыявляемые проблемы. Поэтому на данный момент Rust явным образом запрещает такое поведение.

          Ставить тут Scala в пример не совсем корректно, поскольку основа у языков и способ диспетчеризации очень отличаются. В этом смысле Rust торит совершенно новый путь и пока не ясно, куда он приведет.

          Советую почитать недавний пост Aaron Turon-а на эту тему, он многое проясняет.


          1. senia
            12.02.2017 09:51

            Сравнение со scala тут как раз очень подходит, так как в scala есть эти же проблемы. И не скажу, что они идеально решены.
            Но некоторые все-таки решены. Например добавление реализации тайпкласса в зависимости не приведет к проблемам с компиляцией, так как любую реализацию тайпкласса, кроме тех, что написаны прямо вместе с типом, необходимо явно импортировать.
            Но проблему с хеш-таблицами это не решает, конечно, ибо при разных использованиях хеш-таблицы могут быть импортированы разные реализации. Решения для этого тоже есть, но они довольно нетривиальны.


            1. Halt
              12.02.2017 10:14

              Может оказаться так, что определения вынесли в прелюдию для обеих библиотек, а потом неявно реэкспортировали в третьей. Так что импорты сами по себе хоть и защищают от случайной коллизии, но все равно не дают 100% гарантии. Разумеется, так делать не надо, но…

              С моей точки зрения, проблема именно в том, что существует ненулевая вероятность огрести проблемы при неудачном стечении обстоятельств из-за кода, которым вы не управляете.

              Мне кажется, что наличие этого правила на данном этапе развития языка как раз отражает философию «stability without stagnation», потому что в противном случае, люди стали бы опасаться обновлять библиотеки, что в конечном итоге стало бы причиной стагнации.


  1. xomachine
    11.02.2017 14:42
    +2

    Ну надо же, я как раз недавно писал библиотеку для сериализации/десериализации с похожим принципом действия для Nim. Если кому интересно, вот она + краткий мануал.
    Пусть обилием поддерживаемых форматов она не может похвастаться, но принцип действия, если я правильно всё понял, тот же. Структура, которую нужно сериализовать передается на вход макросу, а он генерирует соответствующие функции для сериализации/десериализации.


  1. dom1n1k
    11.02.2017 19:31
    -7

    И как, сильно быстрее такой сервис, чем пхп, го или нода?


    1. Halt
      11.02.2017 20:49
      +9

      Смотря что вы под этим понимаете.

      Если говорить про сам язык, то да, быстрее. Rust не использует сборщик мусора, код напрямую компилируется в нативные инструкции, а не выполняется в виртуальной машине. Наконец, сама система типов позволяет поддерживать такие инварианты и делать такие оптимизации, которые в традиционных языках делать крайне сложно.

      Если говорить про готовую экосистему для веб-разработки прямо сейчас, то за меня лучше ответит сайт http://www.arewewebyet.org/.

      Отдельные компоненты системы тестировались и демонстрировали весьма приличные показатели, но разумеется это только пол дела.

      Могу посоветовать посмотреть планы сообщества на 2017й год, где хорошо обозначены приоритеты развития.


  1. 0xd34df00d
    12.02.2017 10:38

    То есть, это как Template Haskell, только Rust?


    1. Halt
      12.02.2017 12:01

      Я сам не разбирался с Template Haskell, но по описанию похоже.

      Вообще, Rust проектировался под большим влиянием Haskell, ML и других функциональных языков, так что ничего удивительно, если какие-то концепции были заимствованы.

      Единственное что стоит отметить: в отличие от Haskell, в Rust еще нет типов высших порядков, но исследования в этом направлении ведутся.


  1. Shablonarium
    12.02.2017 21:11
    +1

    Коллеги, может кто-то здесь подскажет как сбилдить XI Editor, который вот этот https://github.com/google/xi-editor


  1. Antervis
    13.02.2017 06:36

    В теории процедурные макросы выглядят очень мощно. Но очень уж многословно для простого случая


    1. Halt
      13.02.2017 06:48

      А в чем многословность? То что надо токены разбирать в AST? Ну по крайней мере это делается один раз в библиотеке.

      Все же, написать #[derive(Eq, PartialEq, Ord, ParialOrd, Hash, Clone, Debug, Serialize, Deserialize)] — это одна строка, а те же методы, реализованные вручную — сотня-две.