Pull to refresh

Comments 28

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

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

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

В следующей статье я как раз планирую это описать.
Ждем-с :)
Забавно что про сам термин «Procedural macro» в вики написано одно предложение, а все остальные ссылки связаны с Rust'ом (например http://sfackler.github.io/syntax-ext-talk/#/). Неужели там все так просто что нечего и читать про эти макросы?
Спасибо, ознакомился :) Про макросы знаю мало ибо не пользовался особо.
Однако остался вопрос — как связаны понятия Procedural и hygienic macro? Я догадываюсь что в Rust макросы поддерживают обе парадигмы… но как из одного следует другое (и следует ли) я не понимаю. Мне не хватает каких-то базовых знаний на которые можно опираться. Как имплементируются системы с макросами, почему и какие бывают альтернативы.

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

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

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

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

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

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

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

P.S.: Этот момент языка все еще является активно обсуждаемым, так что в будущем могут появиться и другие варианты решения.
Надеюсь они придут к какому-то более удобному способу.
Я с этой проблемой столкнулся при написании генераторов для QuickCheck. Добавление оберток помогает, но выглядит грязно.

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

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-а на эту тему, он многое проясняет.
Сравнение со scala тут как раз очень подходит, так как в scala есть эти же проблемы. И не скажу, что они идеально решены.
Но некоторые все-таки решены. Например добавление реализации тайпкласса в зависимости не приведет к проблемам с компиляцией, так как любую реализацию тайпкласса, кроме тех, что написаны прямо вместе с типом, необходимо явно импортировать.
Но проблему с хеш-таблицами это не решает, конечно, ибо при разных использованиях хеш-таблицы могут быть импортированы разные реализации. Решения для этого тоже есть, но они довольно нетривиальны.
Может оказаться так, что определения вынесли в прелюдию для обеих библиотек, а потом неявно реэкспортировали в третьей. Так что импорты сами по себе хоть и защищают от случайной коллизии, но все равно не дают 100% гарантии. Разумеется, так делать не надо, но…

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

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

Код
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
pub struct WorkspaceEdit {
    /// Holds changes to existing resources.
    #[serde(deserialize_with = "deserialize_url_map", serialize_with = "serialize_url_map")]
    pub changes: HashMap<Url, Vec<TextEdit>>, 
    //    changes: { [uri: string]: TextEdit[]; };
}

fn deserialize_url_map<D>(deserializer: D) -> Result<HashMap<Url, Vec<TextEdit>>, D::Error>
    where D: serde::Deserializer
{
    struct UrlMapVisitor;

    impl de::Visitor for UrlMapVisitor {
        type Value = HashMap<Url, Vec<TextEdit>>;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("map")
        }

        fn visit_map<M>(self, mut visitor: M) -> Result<Self::Value, M::Error>
            where M: de::MapVisitor
        {
            let mut values = HashMap::with_capacity(visitor.size_hint().0);

            // While there are entries remaining in the input, add them
            // into our map.
            while let Some((key, value)) = visitor.visit::<url_serde::De<Url>, _>()? {
                values.insert(key.into_inner(), value);
            }

            Ok(values)
        }
    }

    // Instantiate our Visitor and ask the Deserializer to drive
    // it over the input data, resulting in an instance of MyMap.
    deserializer.deserialize_map(UrlMapVisitor)
}

fn serialize_url_map<S>(changes: &HashMap<Url, Vec<TextEdit>>,
                        serializer: S)
                        -> Result<S::Ok, S::Error>
    where S: serde::Serializer
{
    use serde::ser::SerializeMap;

    let mut map = serializer.serialize_map(Some(changes.len()))?;
    for (k, v) in changes {
        map.serialize_key(k.as_str())?;
        map.serialize_value(v)?;
    }
    map.end()
}


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

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

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

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

Могу посоветовать посмотреть планы сообщества на 2017й год, где хорошо обозначены приоритеты развития.
UFO just landed and posted this here
Я сам не разбирался с Template Haskell, но по описанию похоже.

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

Единственное что стоит отметить: в отличие от Haskell, в Rust еще нет типов высших порядков, но исследования в этом направлении ведутся.
В теории процедурные макросы выглядят очень мощно. Но очень уж многословно для простого случая
А в чем многословность? То что надо токены разбирать в AST? Ну по крайней мере это делается один раз в библиотеке.

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

Articles