В прошлой статье мы познакомились с одной из самых интересных возможностей языка Rust — процедурными макросами.


Как и обещал, сегодня я расскажу о том, как писать такие макросы самостоятельно и в чем их принципиальное отличие от печально известных макросов препроцессора в C/C++.


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


Что можно почитать?


Язык Rust развивается очень интенсивно. Издатели, натурально, не успевают и не берутся выпускать книги, поскольку они устаревают еще до того, как на страницах высохнет краска.


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


Тем, кто уже имеет опыт программирования на других языках, и вообще достаточно взрослый, чтобы разбираться самостоятельно, подойдет другая книга. Предполагается, что она лучше подает материал и должна прийти на смену первой книге. А тем, кому нравится учиться на примерах, подойдет Rust by Example.


Людям, знакомым с C++, может быть интересна книга, а точнее porting guide, старающаяся подать материал в сравнении с C++ и делающая акцент на различиях языков и на том, какие проблемы Rust решает лучше.


Если вас интересует история развития языка и взгляд с той стороны баррикад, крайне рекомендую блоги Aaron Turon и Niko Matsakis. Ребята пишут очень живым языком и рассказывают о текущих проблемах языка и о том, как предполагается их решать. Зачастую из этих блогов узнаешь куда больше актуальной информации, чем из других источников.


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


Новое в Rust 1.15


С момента выпуска 1.14 прошло около 6 недель. За это время в новый релиз успели войти 1443 патча (неслабо, правда?) исправляющие баги и добавляющие новые возможности. А буквально на днях появился и хотфикс 1.15.1, с небольшими, но важными исправлениями.


За подробностями можно обратиться к странице анонса или к детальному описанию изменений (changelog). Здесь же мы сконцентрируемся на наиболее заметных изменениях.


Cargo уже взрослый


Cистема сборки компилятора и стандартной библиотеки Rust была переписана на сам Rust с использованием Cargo — стандартного пакетного менеджера и системы сборки, принятой в экосистеме Rust.


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


Теперь файл с названием build.rs, лежащий на одном уровне с Cargo.toml будет интерпретироваться как билд скрипт.


Уже даже завели уже вмержили pull request на удаление всех makefile; интеграция запланирована на релиз 1.17.


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


Новые архитектуры


У раста появилась поддержка уровня Tier 3 для архитектур i686-unknown-openbsd, MSP430, и ARMv5TE. Недавно стало известно, что в релизе LLVM 4.0 появляется поддержка архитектуры микроконтроллеров AVR. Разработчики Rust в курсе этого и уже готовятся почти все сделали для интеграции новой версии LLVM и новой архитектуры.


Более того, уже есть проекты использования Rust в embedded окружении. Разработчики компилятора опрашивают сообщество для выяснения потребностей этой пока малочисленной, но важной группы пользователей.


Быстрее! Выше! Сильнее!


Компилятор стал быстрее. А недавно еще и объявили о том, что система инкрементальной компиляции перешла в фазу бета-тестирования. На моих проектах время компиляции после незначительных изменений уменьшилось с ~20 до ~4 секунд, хотя окончательная линковка все еще занимает приличное время. Пока инкрементальная компиляция работает только в ночных сборках и сильно зависит от характера зависимостей, но прогресс радует.


Алгоритм slice::sort() был переписан и стал намного, намного, намного быстрее. Теперь это гибридная сортировка, реализованная под влиянием Timsort. Раньше использовалась обычная сортировка слиянием.


В C++ мы можем определить перекрывающую специализацию шаблона для некоторого типа, но пока не можем наложить ограничения на то, какие типы вообще могут использоваться для специализации этого шаблона. Работы в этом направлении ведутся, но пока все очень сложно.


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


В частности, в релизе 1.15 была добавлена специализированная реализация метода extend() для Vec<T>, где T: Copy, которая использует простое линейное копирование регионов памяти, что привело к значительному ускорению.


Помимо этого были ускорены реализации методов chars().count(), chars().last(), и char_indices().last().


Поддержка IDE


Этого пока нет в стабильном Rust, но тем не менее новость слишком значительная, чтобы о ней умолчать. Дело в том, что недавно разработчики Rust Language Server объявили о выходе альфа-версии своего детища.


Language Server Protocol это стандартный протокол, который позволяет редакторам и средам разработки общаться на одном языке с компиляторами. Он абстрагирует такие операции, как автодополнение ввода, переход к определению, рефакторинг, работу с буферами и т.д.


Это означает, что любой редактор или IDE, которые поддерживают LSP автоматически получают поддержку всех LSP-совместимых языков.


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


Макросы в Rust


Вернемся к нашим баранам.


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


  • Выделение логически законченных частей кода для многократного использования
  • Выделение несамостоятельных фрагментов кода, ничего не значащих вне своего контекста

Первый принцип больше соответствует традиционной декомпозиции программ: разделению кода на функции, методы, классы и т. п.


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


  • Обычные макросы
  • Процедурные макросы
  • Плагины компилятора

Обычные макросы (в документации macro by example) используются, когда хочется избежать повторения однообразного кода, но выделять его в функцию нерационально, либо невозможно. Макросы vec! или println! являются примерами таких макросов. Задаются декларативным образом. Работают по принципу сопоставления и подстановки по образцу. Реализация основана на базе работы 1986-го года, из которой они получили свое полное название.


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


На данный момент компилятором предусмотрено только использование процедурных макросов для поддержки пользовательских атрибутов derive. В будущем количество сценариев будет расширяться.


Плагины компилятора являются самым мощным, но сложным и нестабильным (в смысле API) средством, которое доступно только в ночных сборках компилятора. В документации приведен пример плагина поддержки римских цифр в качестве числовых литералов.


Пример макроса


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


Макро-вставки возможны практически во всех местах иерархии модуля:


  • внутри выражений
  • в trait и impl блоках
  • в телах функций и методов
  • втеле модуля

Макросы довольно часто применяются в библиотеках, когда приходится определять однотипные конструкции, например серию impl для стандартных типов данных.


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


Осторожно, мозг!
macro_rules! __impl_slice_eq1 {
    ($Lhs: ty, $Rhs: ty) => {
        __impl_slice_eq1! { $Lhs, $Rhs, Sized }
    };

    ($Lhs: ty, $Rhs: ty, $Bound: ident) => {
        #[stable(feature = "rust1", since = "1.0.0")]
        impl<'a, 'b, A: $Bound, B> PartialEq<$Rhs> for $Lhs where A: PartialEq<B> {
            #[inline]
            fn eq(&self, other: &$Rhs) -> bool { self[..] == other[..] }
            #[inline]
            fn ne(&self, other: &$Rhs) -> bool { self[..] != other[..] }
        }
    }
}

__impl_slice_eq1! { Vec<A>, Vec<B> }
__impl_slice_eq1! { Vec<A>, &'b [B] }
__impl_slice_eq1! { Vec<A>, &'b mut [B] }
__impl_slice_eq1! { Cow<'a, [A]>, &'b [B], Clone }
__impl_slice_eq1! { Cow<'a, [A]>, &'b mut [B], Clone }
__impl_slice_eq1! { Cow<'a, [A]>, Vec<B>, Clone }

Мы же рассмотрим более показательный пример. А именно, реализацию макроса vec!, который выполняет роль конструктора для Vec:


macro_rules! vec {
    // Задание через значение и длину: vec![0; 32]
    ( $elem:expr; $n:expr ) => ( $crate::vec::from_elem($elem, $n) );

    // Задание перечислением элементов: vec![1, 2, 3]
    ( $($x:expr),* ) => ( <[_]>::into_vec(box [$($x),*]) );

    // Рекурсивная обработка финальной запятой: vec![1, 2, 3, ]
    ( $($x:expr,)* ) => ( vec![$($x),*] )
}

Макрос работает подобно конструкции match, но на этапе компиляции. Входом для него является фрагмент синтаксического дерева программы. Каждая ветвь состоит из шаблона сопоставления и выражения подстановки, разделенных с помощью =>.


Шаблон сопоставления напоминает регулярные выражения с возможными квантификаторами * и +. Кроме метапеременных через двоеточие указываются еще предполагаемые типы (designator). Например, тип expr соответствует выражению, ident — любому идентификатору, а ty — идентификатору типа. Подробнее про синтаксис макросов написано в руководстве по макросам и в документации, а в porting guide можно найти актуальный разбор макроса vec! с описанием каждой ветви.


Указание типов метапеременных позволяет более точно определить область применимости макроса, а также отловить возможные ошибки.


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


Чистота и порядок


Макрос в Rust должен быть написан так, чтобы генерировать лексически корректный код. Это означает, что не всякий набор символов может быть валидным макросом. Это позволяет избежать многих проблем, связанных с использованием препроцессора в C/C++.


#define SQUARE(a) a*a

int x = SQUARE(my_list.pop_front());
int y = SQUARE(x++);

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


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


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


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


Макросы в Rust:


  • не затеняют переменные
  • не нарушают порядка разбора условий
  • не дают скрытых побочных эффектов
  • не приводят к неопределенному поведению

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


Зато в пределах макроса можно заводить переменные, которые гарантировано не пересекутся с переменными выше по коду. Например, описанный выше макрос vec! можно переписать с использованием промежуточной переменной. Для простоты рассмотрим только основную ветвь:


macro_rules! vec {
    ( $($x:expr),* ) => {
        {
            // Объявляем переменную-аккумулятор
            let mut result = Vec::new();

            // На каждое выражение из $x подставляем свою строку
            $(result.push($x);)*

            // Возвращаем result как результат применения макроса
            result
        }
    };
}

Таким образом, код


let vector = vec![1, 2, 3];

после подстановки макроса будет преобразован в


let vector = {
    let mut result = Vec::new();

    result.push(1);
    result.push(2);
    result.push(3);

    result
};

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


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


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


В качестве подопытного кролика возьмем реализацию автоматически выводимого конструктора #[derive(new)] из соответствующей библиотеки.


С точки зрения пользователя использование будет выглядеть так:


#[macro_use]
extern crate derive_new;

#[derive(new)]
struct Bar {
    x: i32,
    y: String,
}

fn main() {
    let _ = Bar::new(42, "Hello".to_owned());
}

То есть, определив атрибут #[derive(new)] мы попросили компилятор самостоятельно вывести… а что именно? Откуда компилятор поймет, какой именно метод мы ожидаем получить? Давайте разбираться.


Для начала заглянем в исходный код библиотеки, к счастью он не такой большой:


Много буков (75 строк)
#![crate_type = "proc-macro"]

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;

#[proc_macro_derive(new)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input: String = input.to_string();

    let ast = syn::parse_macro_input(&input).expect("Couldn't parse item");

    let result = new_for_struct(ast);

    result.to_string().parse().expect("couldn't parse string to tokens")
}

fn new_for_struct(ast: syn::MacroInput) -> quote::Tokens {
    let name = &ast.ident;
    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
    let doc_comment = format!("Constructs a new `{}`.", name);

    match ast.body {
        syn::Body::Struct(syn::VariantData::Struct(ref fields)) => {
            let args = fields.iter().map(|f| {
                let f_name = &f.ident;
                let ty = &f.ty;
                quote!(#f_name: #ty)
            });
            let inits = fields.iter().map(|f| {
                let f_name = &f.ident;
                quote!(#f_name: #f_name)
            });

            quote! {
                impl #impl_generics #name #ty_generics #where_clause {
                    #[doc = #doc_comment]
                    pub fn new(#(args),*) -> Self {
                        #name { #(inits),* }
                    }
                }
            }
        },
        syn::Body::Struct(syn::VariantData::Unit) => {
            quote! {
                impl #impl_generics #name #ty_generics #where_clause {
                    #[doc = #doc_comment]
                    pub fn new() -> Self {
                        #name
                    }
                }
            }
        },
        syn::Body::Struct(syn::VariantData::Tuple(ref fields)) => {
            let (args, inits): (Vec<_>, Vec<_>) = fields.iter().enumerate().map(|(i, f)| {
                let f_name = syn::Ident::new(format!("value{}", i));
                let ty = &f.ty;
                (quote!(#f_name: #ty), f_name)
            }).unzip();

            quote! {
                impl #impl_generics #name #ty_generics #where_clause {
                    #[doc = #doc_comment]
                    pub fn new(#(args),*) -> Self {
                        #name(#(inits),*)
                    }
                }
            }
        },
        _ => panic!("#[derive(new)] can only be used with structs"),
    }
}

А теперь разберем его по косточкам и попытаемся понять, что он делает.


#![crate_type = "proc-macro"]

extern crate proc_macro;
extern crate syn;

#[macro_use]
extern crate quote;

use proc_macro::TokenStream;

Во первы?х строках библиотеки задается тип специальный единицы трансляции proc-macro, который говорит, что это будет не абы-что, а плагин к компилятору. Затем подключаются необходимые библиотеки proc_macro и syn со всем инструментарием. Первая задает основные типы, вторая — предоставляет средства парсинга Rust кода в абстрактное синтаксическое дерево (AST). В свою очередь, библиотека quote предоставляет очень важный макрос quote! который мы увидим в действии чуть позже.


Наконец, импортируется необходимый тип TokenStream, поскольку он фигурирует в прототипе функции.


Далее следует собственно функция, выступающая в роли точки входа в процедурный макрос:


#[proc_macro_derive(new)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input: String = input.to_string();
    let ast = syn::parse_macro_input(&input).expect("Couldn't parse item");
    let result = new_for_struct(ast);
    result.to_string().parse().expect("couldn't parse string to tokens")
}

Обратите внимание на атрибут proc_macro_derive(new), который говорит компилятору, что эта функция отвечает за #[derive(new)].


На вход она получает набор токенов из компилятора, составляющих тело макроса. На выходе компилятор ожидает получить другой набор токенов, являющихся результатом применения макроса. Таким образом, функция derive() работает как своеобразный фильтр.


Тело функции весьма нехитрое. Сначала мы преобразуем входной набор токенов в строку, а затем разбираем строку как абстрактное синтаксическое дерево. Самое интересное происходит внутри вызова функции new_for_struct(), который принимает AST на вход, а отдает процитированные токены (об этом позже). Наконец, полученные токены преобразуются обратно в строку (не спрашивайте меня, почему так), парсятся в TokenStream и отдаются уже в качестве результата работы макроса компилятору.


Если честно, я тоже не понимаю, зачем тасовать данные туда-сюда через строки и почему нельзя было сразу сделать вменяемый интерфейс, ну да ладно. Возможно, в будущем ситуация изменится.


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


Итак, на вход нам могут подать:


// Обычная структура
#[derive(new)]
struct Normal {
    x: i32,
    y: String,
}

// Вариант tuple struct
#[derive(new)]
struct Tuple(i32, i32, i32);

// Структура-пустышка
#[derive(new)]
struct Empty;

Понятное дело, что синтаксические деревья у всех трех вариантов будут различными. И это нужно учитывать при генерировании метода new(). Собственно, все что делает new_for_struct(), — это смотрит на переданное AST дерево, определяет, с каким вариантом она имеет дело данный момент и генерирует нужную подстановку. А если ей на вход передали незнамо что — она начинает паниковать.


fn new_for_struct(ast: syn::MacroInput) -> quote::Tokens {
    let name = &ast.ident;
    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
    let doc_comment = format!("Constructs a new `{}`.", name);

    match ast.body {
        syn::Body::Struct(syn::VariantData::Struct(ref fields)) => { /* обычная структура */ },
        syn::Body::Struct(syn::VariantData::Unit) => { /* единичный тип */ },
        syn::Body::Struct(syn::VariantData::Tuple(ref fields)) => { /* tuple struct */ }
        _ => panic!("#[derive(new)] can only be used with structs"),
    }
}

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


// Для каждого поля в структуре генерируем список пар имя:тип
// которые позже используем в списке параметров конструктора
let args = fields.iter().map(|f| {
    let f_name = &f.ident;
    let ty = &f.ty;
    quote!(#f_name: #ty)
});

// Генерируем тело конструктора, пары имя:имя
let inits = fields.iter().map(|f| {
    let f_name = &f.ident;
    quote!(#f_name: #f_name)
});

// Наконец, собираем все воедино и цитируем весь блок целиком:
quote! {
    impl #impl_generics #name #ty_generics #where_clause {
        #[doc = #doc_comment]
        pub fn new(#(args),*) -> Self {
            #name { #(inits),* }
        }
    }
}

Вся хитрость здесь заключена в макросе quote! который позволяет цитировать фрагменты кода, подставляя вместо себя набор соответствующих токенов. Обратите внимание на метапеременные, начинающиеся с решетки. Они унаследованы из лексического контекста, в котором находится цитата.


Если все еще не понятно «как оно работает», взгляните на результат применения процедурного макроса к описанной выше структуре Normal.


Сама структура еще раз:


#[derive(new)]
struct Normal {
    x: i32,
    y: String,
}

Результат применения процедурного макроса:


/// Constructs a new `Normal`.
impl Normal {
    pub fn new(x: i32, y: String) -> Self {
        Normal { x: x, y: y }
    }
}

Внезапно, все становится на свои места. Оказывается, мы только что собственноручно сгенерировали impl блок для структуры, добавили в него ассоциированную функцию-конструктор new() с документацией (!), двумя параметрами x и y соответствующих типов и с реализацией, которая возвращает нашу структуру, последовательно инициализируя ее поля значениями из своих параметров.


Поскольку Rust может понять из контекста, чему соответствуют x и y до и после двоеточия, все компилируется успешно.


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


Заключение


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


Надеюсь, что статья оказалась вам полезной. А если после ее прочтения вы еще и захотели поиграться с языком Rust, я буду считать свою задачу выполненной полностью :)


Материал подготовлен совместно с Дарьей Щетининой.

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

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


  1. Door
    14.02.2017 22:49
    +1

    Не пытался садится за Rust всерьёз из-за синтаксиса. Он меня жутко пугает. Что посоветуете? Потерпеть сначала, а потом будет всё в порядке?


    1. snuk182
      14.02.2017 23:00
      +8

      Не бойтесь ни грамма. Документация, в том числе русскоязычная — великолепна. Главное внимательно почитать и потренировать часть про времена жизни. Ну и если вы пришли из VM- или скриптового языка — различия стека и кучи.


    1. TargetSan
      15.02.2017 00:21
      +8

      Это Вы С++ во всей его красе наверное не видели


      1. Door
        15.02.2017 06:51

        Я настолько привык к C++, что жить без него не могу и уже не замечаю проблемы с синтаксисом )
        Спасибо за советы. Пойду смотреть Rust


        1. DarkEld3r
          15.02.2017 13:24
          +4

          Тоже, в основном, на С++ пишу, но раст отторжения совсем не вызвал. Возможно, помогло изучение других языков.


          1. Door
            15.02.2017 14:45
            +3

            Отторжения нет. Мне уже нравится ^_^


    1. Halt
      15.02.2017 06:47
      +7

      А действительно, расскажите пожалуйста для статистики, какие именно места синтаксиса кажутся вам наиболее срашными? Постараемся разобраться вместе.

      Мне тоже вначале было непривычно и местами неприятно, потом привык и перестал обращать внимание. Лично мне не нравится всеобщий сокращизм и стремление писать ключевые слова и идентификаторы в 2-3 символа. Другие наоборот фанатеют от этого.

      Вообще, после того, как наешься радости production style C/C++… когда устанешь разгребать тонны кода от именитых компаний и брендов, который на поверку оказывается банальным индийским *, то начинаешь ценить в первую очередь не синтаксис, не абстрактную выразительность или там лаконичность языка, а то, какие проблемы он решает и какие гарантии он дает.

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

      Конечно, есть unsafe блоки, которыми можно устроить ад и в Rust, но их можно очень быстро отсмотреть. Можно даже настроить систему так, чтобы геррит/гитхаб разрешал ревьюить код, содержащий unsafe блоки только сениорам, тогда как safe Rust можно отдавать на ревью кому угодно.

      Например, бот Rust Highfive выдает большое жирное предупреждение на pull request, который модифицирует unsafe код. Сразу дает понять, что тут могут быть серьезные баги и надо ревьюить особенно внимательно.


      1. Door
        15.02.2017 09:14
        +3

        Спасибо за комментарий.
        Предупреждаю — ниже взгляд человека, который НЕ разбирался в языке совершенно ни грамма.


        Навскидку:


        name: &'static str,

        этот маленький ' крючёчек для меня, как человека с плохим зрением — просто сложно заметен.
        Непривычно смотрится:


        &self, f: &mut Formatter

        но это просто нужно один раз понять (я не разбирался что всё это значит ещё. Догадываюсь, что, возможно, в Rust-е всё const-by-default, поэтому &mut. А & — потому что, так же, как и в плюсах, есть возможность передавать по ссылке/по значению. Но опять же — это просто догадки).


        Непривычно смотрится просто знак восклицания println! и всякие штуки, типа slice: &[i32].
        Дальше:


        // A tuple struct
        struct Pair(i32, f32);

        только с комментария догадываюсь, что это аналог алиаса (?): using Pair = std::tuple<i32, f32>; и, возможно, доступ будет происходить по индексам.


        Ну, а этот код из статьи — это вообще ад:


        macro_rules! __impl_slice_eq1 {
            ($Lhs: ty, $Rhs: ty) => {
                __impl_slice_eq1! { $Lhs, $Rhs, Sized }
            };
        
            ($Lhs: ty, $Rhs: ty, $Bound: ident) => {
                #[stable(feature = "rust1", since = "1.0.0")]
                impl<'a, 'b, A: $Bound, B> PartialEq<$Rhs> for $Lhs where A: PartialEq<B> {
                    #[inline]
                    fn eq(&self, other: &$Rhs) -> bool { self[..] == other[..] }
                    #[inline]
                    fn ne(&self, other: &$Rhs) -> bool { self[..] != other[..] }
                }
            }
        }

        Опять же — уверен, что если разобраться — всё становиться логичным.
        Спасибо


        1. Halt
          15.02.2017 09:33
          +4

          Очень многое вы угадали верно :) Давайте по порядку.

          этот маленький ' крючёчек для меня, как человека с плохим зрением — просто сложно заметен.
          Непривычно смотрится:
          Идентификаторы с, как вы его называете крючком, это лайфтаймы. В отличие от С++, где понятия времени жизни объекта и отношения владения не выражены в коде явно, в Rust это элемент синтаксиса.

          Когда в C++ вы принимаете аргументом конструктора и сохраняете в поле некую ссылку, вы неявно говорите «все взорвется, если конструируемый объект переживет тот, на который ссылается». Вам остается только надеяться что другие программисты не дураки и не передадут вам ссылку на временный объект.

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

          Идентификаторы с крючком были введены, чтобы отличать их от других шаблонных параметров и чтобы их можно было аккуратно прилепить к ссылке:

          fn my_function<'a, T>(reference: &'a T) -> &'a T;
          



        1. snuk182
          15.02.2017 09:46
          +5

          Если вкратце — по умолчанию все параметры передаются по значению без копирования. То есть если вы передали что-то в другую функцию без &, больше вы его использовать не сможете. Это называется передача владения объектом. Передача же по ссылке производится явно, амперсандом (&). При этом владения объектом не происходит, и после возврата можно пользоваться им как обычно. Но одновременно можно заимствовать только неизменяемые ссылки, на которых нет в это же время изменяемой ссылки.
          Как вы верно заметили, по умолчанию все ссылки и объекты неизменяемые. Изменяем ость нужно явно прописывать словом mut.
          Знак восклицания после имени функции говорит о том, что это макрос. Вы ими пока голову не забывайте, есть темы поважнее.


          1. Door
            15.02.2017 14:41
            +1

            Спасибо.


            все параметры передаются по значению без копирования. То есть если вы передали что-то в другую функцию без &, больше вы его использовать не сможете.

            По сути, в плюсах — это аналог rvalue с мув семантикой. Но в плюсах нет поддержки овнершипа на уровне языка/компилятора:


            std::string str = "I'm the owner";
            foo(std::move(str)); // передали овнершип в функцию

            Как я уже писал Halt — смотрю Rust из-за этих манипуляций с лайфтаймом :)


            1. snuk182
              15.02.2017 18:36
              +3

              При работе с вж есть несколько первоочередных правил:
              1. Если у вас ссылочный только аргумент функции — вж можно не ставить, оно по умолчанию будет считаться из той области видимости, где функция вызывается. Касается как self, так и просто параметров.
              fn do(arg: &Type) { ... }

              2. Если у вас ссылочный только результат функции с self — вж тоже можно не писать, оно привязывается к вж self (кроме случаев, когда вж результата зависит от другого параметра, и вам надо это явно указать)
              fn do(&self) -> &T { &self.member_of_type_t } //например

              3. Ясное дело, что ссылку на созданный внутри функции объект возвращать нельзя — dangling pointer. Для такого есть Box — ссылка на объект в куче.
              fn make_stack() -> &T { ... } //EPIC FAIL
              fn make_heap() -> Box<T> { ... } //EPIC WIN

              4. Не создавайте функции с нессылочным self без крайней необходимости — они уничтожают self (из правила, написанного мной выше)
              fn kill_self(self) { ... }

              5. Если у вас член структуры — ссылка, вы обязаны ее вж таскать с этой структурой везде. Логично, потому что если ссылающийся объект помрет без нашего ведома, мы получим segfault.
              struct Container<'a> {
              memb: &'a Item
              }


              1. DarkEld3r
                15.02.2017 19:35
                +5

                Если у вас ссылочный только аргумент функции — вж можно не ставить

                Я бы даже сказал иначе: не стоит бросаться лайфтамы прописывать пока компилятор не заставит. (:


        1. Halt
          15.02.2017 10:39
          +2

          только с комментария догадываюсь, что это аналог алиаса (?): using Pair = std::tuple<i32, f32>; и, возможно, доступ будет происходить по индексам.

          Вот тут не угадали :) Кортежи в Rust есть и они пишутся просто скобками:
          // Функция, возвращающая кортеж
          fn get_tuple() -> (i32, f32) {
              (42, 3.14)
          }
          
          // Деструктурирующее связывание:
          let (x, y) = get_tuple();
          
          // Можно и по индексам:
          let t = get_tuple();
          let x = get_tuple().0;
          let x = t.1;
          


          Структуры вида struct Pair(i32, f32); служат для другой цели. Их используют когда хочется связать несколько значений под одним общим именем типа, но отдельные поля именовать не хочется. Сюда можно отнести координаты, группы однотипных значений и т. д.

          Есть даже вырожденный паттерн, например struct Meters(f32) который может показаться бессмысленным, но на самом деле используется для повышения надежности программ.

          Ну, а этот код из статьи — это вообще ад:
          Дело в том, что это не простой код, это макрос. Идентификаторы с восклицательными знаками это тоже все макросы. Сделано специально так, чтобы их можно было отличить от обычных функций.

          Такие конструкции можно сравнить с шаблонной магией в C++ которая очень мощная, но может быть довольно трудночитаема (загляните в boost или в stdlib).

          Основная идея в том, чтобы упаковать весь ад внутрь абстракции, а наружу выставить вполне милый интерфейс, типа vec![], который скрывает всю сложность, но дает удобство простому пользователю.


          1. Door
            15.02.2017 14:36

            Спасибо за объяснения.


            Структуры вида struct Pair(i32, f32); служат для другой цели. Их используют когда хочется связать несколько значений под одним общим именем типа, но отдельные поля именовать не хочется.

            По сути (очень грубо) это и есть тюплы. Просто это strong typedef (т.е., алиас, который кроме синонима вводит ещё и новый тип на основе которого можно делать перегрузку ф-й и прочее. То, чего не хватает плюсам на уровне языка).


            struct Meters(f32) — подтверждает это. На псевдо коде мы имеем что-то такого:


            void foo(f32); // 1
            void foo(Meters); // 2
            
            foo(f32{0}); // 1
            foo(Meters{0}); // 2

            (Подправте, если ошибаюсь)


            С макросами — понял — аналог шаблонов. Но тогда


            Идентификаторы с крючком были введены, чтобы отличать их от других шаблонных параметров и чтобы их можно было аккуратно прилепить к ссылке:

            есть и макросы (которые судя по комментариям ниже могут генерить код/модули) и есть шаблоны.


            Спасибо за объяснения. Я фан явного указания жизни обьектов. Из-за вас теперь уже точно смотрю на Rust :)


            1. splav_asv
              15.02.2017 14:50

              Грубо: макросы — кодогенерация, причём гигиеничная. Шаблоны — обобщённое программирование.


              1. snuk182
                15.02.2017 15:17
                +4

                Добавлю: макросы Rust работают с синтаксическим деревом, а не с текстом, что исключает сишную define-анархию в принципе. Есть немногочисленные недочеты в том, как это работает сейчас (да, concat_idents!, я смотрю на тебя), но главный постулат языка — безопасность — нигде не нарушается.


            1. Halt
              15.02.2017 14:54
              +1

              Дженерики (шаблоны) в Rust — это реализация параметрического полиморфизма. Смысл у них примерно тот же что и у шаблонов C++.

              Говорить что «макросы — аналог шаблонов» довольно странно, все равно как в контексте C++ говорить что, «#define — это аналог template».

              Либо я вас не понял.


            1. Sirikid
              16.02.2017 03:43
              +2

              Таплы и есть, чистый тип-сумма, только в C++ они эмулируются шаблонными структурами, а в Rust объекты первого класса.


        1. VadimVP
          15.02.2017 11:04
          +1

          Ну, а этот код из статьи — это вообще ад:

          Сорри.
          (Это я писал.)


      1. grossws
        15.02.2017 12:14
        +3

        Конечно, есть unsafe блоки, которыми можно устроить ад и в Rust, но их можно очень быстро отсмотреть. Можно даже настроить систему так, чтобы геррит/гитхаб разрешал ревьюить код, содержащий unsafe блоки только сениорам, тогда как safe Rust можно отдавать на ревью кому угодно.

        Например, бот Rust Highfive выдает большое жирное предупреждение на pull request, который модифицирует unsafe код. Сразу дает понять, что тут могут быть серьезные баги и надо ревьюить особенно внимательно.

        Строго говоря, наличие unsafe в рамках модуля уже требует аккуратного review всего кода в модуле, в том числе safe, т. к. он тоже может нарушить инварианты, на которые полагается unsafe часть. Пример, вроде, был в Nomicon'е.


        1. Halt
          15.02.2017 12:18

          Спасибо за комментарий! А вы не можете дать ссылку на этот момент? Я, к сожалению, такого не припомню.


          1. grossws
            15.02.2017 12:26
            +2

            https://doc.rust-lang.org/nomicon/working-with-unsafe.html, там где рассматривается игрушечный Vec.


            1. Halt
              15.02.2017 12:54

              Действительно, очень заковыристая ситуация. То есть rule of thumb такой: любой модуль, содержащий unsafe блоки должен считаться unsafe. А вот граница модулей таки обеспечивает необходимый уровень изоляции.

              Ну и вот это надо запомнить:

              Unsafe code must trust some Safe code, but can't trust generic Safe code. It can't trust an arbitrary implementor of a trait or any function that was passed to it to be well-behaved in a way that safe code doesn't care about.


    1. Amomum
      15.02.2017 14:16

      Меня, например, пугает в первую очередь какая-то многословность синтаксиса в целом. При этом я понимаю, что явное лучше неявного и все такое, но некоторые куски вызывают ступор просто из-за обилия спецсимволов:

      fn escape_str(wr: &mut fmt::Write, v: &str) -> EncodeResult 
      

      fn tcx<'a>(&'a self) -> TyCtxt<'a, 'gcx, 'tcx>;
      


      На это накладывается все еще непривычный синтаксис «имя: Тип», использование угловых скобок как для лайфтаймов, так и для «шаблонных» параметров.

      Кстати, об угловых скобках. Относительно частая необходимость вложенных шаблонных параметров вызывает чисто читательский дискомфорт, ведь угловые скобки — на самом деле не скобки, большинство IDE не подсвечивает их правильно и чисто визуально они воспринимаются хуже, чем круглые или квадратные (а я-то гадал, почему в Scala дженерики используют квадратные скобки):
      fn ast_ty_to_ty_cache(&self) -> &RefCell<NodeMap<Ty<'tcx>>>;
      

      Имхо, не стоило из С++ брать угловые скобки для шаблонов и :: для неймспейсов, все равно ведь остальной синтаксис не очень похож, да и способ мышления все равно совсем другой нужен.
      Но тут, видимо, уже ничего не поделаешь.


      1. Halt
        15.02.2017 14:45
        +3

        Использование угловых скобок как для лайфтаймов, так и для «шаблонных» параметров имеет вполне конкретный смысл: и то и другое это проявление параметрического полиморфизма. В C++ код может быть generic over type, в Rust к добавок к этому он может быть generic over lifetime. Поэтому синтаксис здесь общий.

        Синтаксис «имя: тип» я думаю выбран потому, что в отличие от C++ где аннотации типа обязательны, в Rust они являются скорее исключением и обязательны только в прототипах функций и структурах. В подавляющем большинстве случаев, типы внутри метода могут быть выведены полностью.

        Помимо этого, аннотация типа через двоеточие активно применяется в научном сообществе особенно в работах по теориям типов и лямбда исчислению и в целом довольно интуитивна:

        let x = 42;
        let y: String = "hello_world".to_owned();
        

        Делать же два разных синтаксиса объявления с аннотацией типа и без оной — еще хуже. Если делать по аналогии с C/C++

        let x = 42;
        String y = "hello_world".to_owned();
        

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


        1. Amomum
          15.02.2017 14:55

          Синтаксис «имя: тип» я думаю выбран потому, что в отличие от C++ где аннотации типа обязательны, в Rust они являются скорее исключением и обязательны только в прототипах функций и структурах. В подавляющем большинстве случаев, типы внутри метода могут быть выведены полностью.

          К этому моменту у меня претензий нет, когда изначально есть вывод типов через let (а не прикостыленное auto), это выглядит вполне органично.
          А возвращаемое значение у функции через -> позволяет избавиться от безумного сишного синтаксиса указателей на функции. Это все понятно, просто непривычно.

          Но вот угловые скобки вообще — это все-таки не очень.
          Сравните:
          Box<Map<Vec<i8>>>
          Box[Map[Vec[i8]]]
          Box(Map(Vec(i8)))
          


          Для угловых скобок еще и шифт надо нажимать.


          1. Halt
            15.02.2017 15:11
            +1

            В C++ символ * в зависимости от контекста может означать:

            • оператор умножения: 2 * 3
            • указатель: char*
            • разадресацию указателя: *pint = 42
            • пользовательский operator *
            • наверняка, я еще что-то забыл, какие-нибудь триграфы

            Компилятору C++ уже приходится разгребать завалы звездочек и применять шаблоны только чтобы построить AST.

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

            Это упрощает парсер и облегчает чтение кода. Особенно это важно, поскольку на этапе обработки макросов уже должно быть известно, чему соответствует каждый идентификатор.

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

            Надо сказать, что разработчики довольно много времени уделяют вопросам так что с большой вероятностью то, что вы сейчас видите — это действительно оптимальный вариант. Например, по поводу синтаксиса для лайфтаймов в свое время было очень много споров, но никто в итоге так и не смог предложить лучшего варианта с учетом всех «за» и «против».

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


            1. Amomum
              15.02.2017 15:23
              +3

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


              1. Optik
                15.02.2017 15:38
                +3

                [mode=«zanuda»]В скалке a(...) это сокращенный вызов метода a.apply(...). Работает для любого объекта с этим методом. [/mode]


                1. grossws
                  15.02.2017 18:59
                  +1

                  плюс ещё update: a(1) = "xxx" эквивалентно a.update(1, "xxx")


                  1. Halt
                    16.02.2017 12:31

                    Справедливости ради стоит сказать, что в Rust такое тоже есть.

                    Любой метод с &self можно вызывать напрямую как обычную функцию, принимающую первым параметром объект:

                    struct Foo(u32);
                    
                    impl Foo {
                        pub fn bar(&self, arg: u32) -> u32 { self.0 + arg }
                    }
                    
                    fn main() {
                        let foo = Foo(42);
                        assert_eq!(Foo::bar(&foo, 1), 43);
                    }
                    


                    1. grossws
                      16.02.2017 12:58
                      +3

                      То что упомянул Optik и про что написал я к ufcs особого отношения не имеет.


                      Разговор про синтаксический сахар, который позволяет делать read-/update-like методы. Это несколько похоже на operator() в плюсах (для read варианта). Т.е. self/this по-прежнему передается неявно (в отличии от того, что разрешает ufcs).


                      1. grossws
                        16.02.2017 13:03

                        Т.е. a(x, y) как rvalue означает (эквивалентно) a.apply(x, y), а a(x, y) = z === a.update(x, y, z), IIRC


                      1. Halt
                        16.02.2017 13:05

                        Тогда это больше похоже на стандартные типажи операций.

                        Например, a + b можно вызвать и как a.add(b).


                        1. grossws
                          16.02.2017 13:15
                          +3

                          Это ближе, да. Только в случае скалы нет прибитого гвоздем количества аргументов, как если аналогичную вещь в расте реализовать через новый trait в std::ops.


                          Scala в смысле гибкости синтаксиса прекрасна. И, порой, ровно поэтому же ужасна: придумает кто-нибудь в очередной библиотеке оператор «гибкий кораблик» a <=?=> b, и думай после этого где его искать и как по него гуглить (по мотивам реальной истории про аналогичный оператор).


                          1. Halt
                            16.02.2017 13:19

                            Haskell тоже этим грешит и мне это откровенно не нравится. Мне кажется, что операторы таки должны быть стандартными, хотя и на стандартном наборе умудряются делать неочевидные или контринтуитивные вещи.


                            1. Sirikid
                              16.02.2017 19:46
                              +1

                              А как же Hoogle?


                              1. Halt
                                17.02.2017 08:18

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

                                С другой стороны, если IDE может нормально прожевать весь синтаксис и подсветить оператор именно в контексте текущего использования (как делает KDevelop для C++), то пожалуйста, лишь бы это было оправдано.


              1. Halt
                15.02.2017 15:57
                +1

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

                Пока шел домой понял, что пример со звездочками не очень, ибо Rust-у, в общем-то, приходится делать то же самое. Гораздо показательнее в отношении C++ такой пример:

                Где-то в тексте программы есть конструкция (a)(b) где a и b — некие идентификаторы. Надо выписать все возможные конструкции, которые могут за этим скрываться.

                Вот тут уже без знания фактических типов a и b ничего распарсить не получится.


                1. senia
                  15.02.2017 21:59

                  Где-то в тексте программы есть конструкция (a)(b) где a и b — некие идентификаторы. Надо выписать все возможные конструкции, которые могут за этим скрываться.
                  Расскажите тем, кто шапочно знаком с C++, что там может скрываться.


                  1. ZyXI
                    15.02.2017 23:57
                    +3

                    В C здесь может скрываться


                    1. Приведение типа b к a. В C++ тоже есть.
                    2. Вызов функции (я даже как?то писал (cond ? func1 : func2)(args)) по указателю на функцию. В C++ вроде тоже должно быть.
                    3. Только C++: вызов оператора вызова функции у b.
                    4. Объявление функции. Компилятор по?умолчанию подставляет int как возвращаемый тип, но если на контекст ограничений нет, то можно увидеть rettype (funcname)(argtype) и без нестандартного поведения компилятора.
                    5. Вроде в C++ это может быть частью объявления с инициализацией.

                    В общем, три варианта только в C, и я ещё не уверен, что перечислил все. Кстати, если на контекст ограничений совсем нет, то ещё есть варианты «это часть строкового литерала», «это часть комментария» и «это то же, что и (b), потому что MACRO(a) раскрывается в пустоту».


                    1. Halt
                      16.02.2017 07:09
                      +1

                      Помимо этого (в зависимости от того, насколько вольно можно интерпретировать выражения a и b и что, что стоит с краев), в C++ это еще может быть:

                      1. Часть объявления переменной (да, так можно и в C): (int)(x);
                      2. Часть более длинной цепочки приведения (a)(b) …c
                      3. Вызов оператора () у объекта a с параметром b уже сказали
                      4. Цепочка вызовов оператора (): foo(a)(b)(c)
                      5. Вызов функции-члена через указатель на нее, не то же самое, что просто через указатель на функцию
                      6. Наверняка еще что-то забыл


                      1. Halt
                        16.02.2017 07:33

                        Еще: левые скобки могут быть частью конструктора, а правые — применением оператора к полученному объекту: MyClass(a)(b).


                      1. Halt
                        16.02.2017 08:11
                        +1

                        Если же читерить с макросами, то можно вообще разгуляться:

                        • Конструктор через вызов placement new, завернутый в макрос
                        • Вызов любых операторов через синтаксис x.operator Y()


                      1. Halt
                        16.02.2017 08:31

                        Про (int)(x) я похоже наврал, хотя вроде бы такое было возможно в старых компилтяорах. Сейчас проглатывает только вариант int (x); Зато таки можно написать int (x)(2);


              1. Sirikid
                16.02.2017 03:51
                +2

                В Scala поэтому круглые скобки используются в том числе для доступа к массивам.

                И в Ada, мне тоже нравится этот вариант, особенно если не делать массивы объектами первого класса.


                А в Lisp как-то обошлись только круглыми скобками.

                А вы попробуйте добавить в синтаксис лиспа аннотации типов :) Думаю можно обойтись только круглыми скобками, но в определенный момент 90% кода будут составлять макро определяющие более удобный синтаксис.


                1. DarkEld3r
                  16.02.2017 13:33
                  +1

                  А вы попробуйте добавить в синтаксис лиспа аннотации типов :)

                  У Racket есть типизированный диалект, выглядит вот так:


                  (define (foo [a : Number] [b : Number]) : Number
                    (let ([c : Number (+ a b)])
                      (* 2 c)))
                  
                  (define (bar (a : Number) (b : Number)) : Number
                    (let ((c : Number (+ a b)))
                      (* 2 c)))

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


                  P.S. Вывод типов (для let, например) тоже есть.


                  1. Sirikid
                    16.02.2017 19:52
                    +1

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

                    Мне кажется или вы подтвердили мое предположение? :^) Я бы предпочел префиксную форму :, это все таки лисп. (Я в курсе что она есть.)


                    1. DarkEld3r
                      17.02.2017 01:02
                      +1

                      Мне кажется или вы подтвердили мое предположение? :^)

                      Я с ним и не спорил. (:


                      Даже в не типизированном или, вернее, динамически типизированном варианте языка используются квадратные скобки. Правда сам на ракете не пишу, просто изучал из интереса, но кажется, что читаемость это заметно повышает. Есть ещё практика раскрашивать (в IDE) скобки в разные цвета, может тоже помогает, если приноровиться, не могу судить.


                      Насчёт префиксной формы не совсем понял, речь о следующем?


                      (: foo (Number Number -> Number))
                      (define (foo a b)
                        (let ([c (+ a b)])
                          (* 2 c)))

                      Могу ошибаться, но сделано оно скорее для упрощения добавления типов в имеющийся код и, кажется, работает не везде. Например, разве можно такую форму с let использовать?


                      1. Sirikid
                        17.02.2017 04:28
                        +2

                        Насчёт префиксной формы не совсем понял, речь о следующем?

                        Да.


                        Могу ошибаться, но сделано оно скорее для упрощения добавления типов в имеющийся код и, кажется, работает не везде. Например, разве можно такую форму с let использовать?

                        По идее это его естественный способ записи для лиспа — (форма аргумент1 аргумент2 ...). Насчет let не в курсе.


                        В Haskell, кстати, тоже используется такая форма записи — типы отдельно, функция отдельно.


                        foo :: Integer -> Integer -> Integer
                        foo a b =
                          let c = (+) a b
                          in (*) 2 c

                        Есть ещё практика раскрашивать (в IDE) скобки в разные цвета, может тоже помогает, если приноровиться, не могу судить.

                        Мне кажется rainbow delimiters нужены только если редактор не умеет подсвечивать парные скобки.


                        Можно просто по другому форматировать код, я стараюсь писать так чтобы действие с одной строкой кода в VCS отображалось именно как действие с одной строкой. Закрывающие скобки не скапливаются в конце одной строки и находить парные становится легче.


                        Как обычно форматируют
                        (defun delete-spaces-after (&optional start-point)
                          (interactive)
                          (let* ((from (or start-point (point)))
                                 (to (progn
                                       (skip-chars-forward " \t")
                                       (constrain-to-field nil from t))))
                            (delete-region from to)))
                        
                        (defun delete-spaces-before (&optional start-point)
                          (interactive)
                          (let* ((to (or start-point (point)))
                                 (from (progn
                                         (skip-chars-backward " \t")
                                         (constrain-to-field nil to))))
                            (delete-region from to)))
                        
                        (defun my-kill-line (&optional arg)
                          (interactive "P")
                          (kill-line arg)
                          (delete-spaces-after (point)))


                        1. Halt
                          17.02.2017 06:32

                          Возвращаясь к исходному замечанию про угловые скобки в шаблонах.

                          Мне кажется, что все-таки проблемы индейцев шерифа волновать не должны. В том смысле, что подсветка синтаксиса языка — это обязанность редактора.

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


                        1. DarkEld3r
                          17.02.2017 12:59
                          +1

                          Про хаскель в курсе, опять же, кажется, что там нередко принято проставлять типы только в нужных местах. Мне всё-таки больше по вкусу подход раста, где для функций их указывать обязательно. Ну и мне как-то больше по душе когда типы идут вместе с функцией, а не отдельно.


                          Насчёт скобочек в лиспе — вроде как "тру лисперы" такое порицают, да и (наверное) не зря автоформатирование расставляет их не как в других языках. Ну и когда я лисп щупал, то хотелось побольше "проникнуться", так что подстраивался под местные обычаи. (:


                          1. Halt
                            17.02.2017 13:09
                            +1

                            В современном Haskell принято писать прототипы функций.

                            В этом смысле это практически эквивалентно тому, что происходит в Rust, за исключением того факта, что в Haskell это правило хорошего тона, а не жесткое требование компилятора.

                            Выше Sirikid написал, как оно обычно выглядит.


  1. wikipro
    15.02.2017 09:26
    +5

    Rust уже достаточно проработан чтобы на нём написать линукс подобную ОС Redox — если кому любопытно


  1. VadimVP
    15.02.2017 11:01
    +2

    Если честно, я тоже не понимаю, зачем тасовать данные туда-сюда через строки и почему нельзя было сразу сделать вменяемый интерфейс

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


    Cистема сборки компилятора и стандартной библиотеки Rust была переписана на сам Rust с использованием Cargo

    Ууу, rustbuild, сраное говно.
    Вместо того, чтобы вести себя как приличные системы сборки и работать с графом зависимостей с таргетами в узлах и командами привязанными к таргетам, это наколеночное поделие херачит большую часть билда и тесты подряд в императивном стиле, режет параллелизм, добавляет зависимости. Когда совсем приспичивает приходится сравнивать timestamp'ы вручную (!) кодом на Rust.
    Как же его хочется переписать, но нет возможности.
    Хотя читается, конечно на порядок лучше старых мейкфайлов.


    1. Halt
      15.02.2017 11:54
      +3

      Спасибо за комментарий!

      Поэтому пока сделали ровно необходимый минимум.
      По поводу API я так и предполагал. Я так понимаю, в будущем будет возможность конвертации между `quote::Tokens` и `TokenStream`, или же оно вообще будет приведено к одному типу? Ну или через From/Into/AsRef.

      Ууу, rustbuild, сраное говно.
      А можно подробнее? Что куриличем руководствовались разработчики, почему было сделано именно так? Есть ли конкретные обрисованные планы развития? Вообще, я так понимаю, можно ответить вашими же словами, про то что сделали чтобы было, а потом это все будет допиливаться.


      1. VadimVP
        15.02.2017 15:47
        +1

        По поводу API я так и предполагал. Я так понимаю, в будущем будет возможность конвертации между quote::Tokens и TokenStream, или же оно вообще будет приведено к одному типу? Ну или через From/Into/AsRef.

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


        Что куриличем руководствовались разработчики, почему было сделано именно так?


        (a) leverage Cargo as much as possible and failing that (b) leverage Rust as much as possible!


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

        Да, разумеется, функционально оно работает, компилятор и библиотеку собирает и тестирует, можно дальше улучшать. Новые фичи добавляются на порядок легче по сравнению с мейкфайлами.
        То что дизайн некошерный, это вопрос ортогональный. Меня в основном беспокоит то, что для контрибьюторов, которые хотят собирать и тестировать локально, rustbuild хуже чем старая система.


        Есть ли конкретные обрисованные планы развития?

        Важная инфраструктура (производство релизных сборок, расширение тестирования для разных платформ) будет расширяться, в остальном расчет на то что сторонние контрибьюторы будут подчищать конюшни.
        Одна из целей переписывания системы сборки было как раз привлечение новых контрибьюторов.


        1. Sirikid
          16.02.2017 03:56

          расчет на то что сторонние контрибьюторы будут подчищать конюшни

          А можно какой-нибудь пост или faq как вкатится в эти конюшни? Как правильно писать патчи etc.


          1. VadimVP
            16.02.2017 10:53
            +3

            В CONTRIBUTING.md достаточно подробно всё описано (в том числе как искать entry-level тикеты).
            Для начала рекомендую найти какую-нибодь опечатку или функцию с плохой документацией, исправить и сдалать pull request на гитхабе просто чтобы опробовать процесс. Патчей такого рода много, ревьюеры доброжелательные и если что-то не так всегда подскажут, поправят + ревью и мердж достаточно оперативно происходят, не надо ждать месяц пока maintainer соизволит взглянуть на патч, как в некоторых проектах.


            1. grossws
              16.02.2017 11:49

              не надо ждать месяц пока maintainer соизволит взглянуть на патч, как в некоторых проектах

              в некоторых проектах месяц — это очень повезло, а реальное время 3-9 месяцев, вот где затаилось зло


  1. dimack
    15.02.2017 13:27
    +4

    Меня все время мучали вопросы по поводу синтаксиса:

    1. Почему у макросов нет пространства имен? Это же неудобно, начиная с того, что непонятно из какого пакета он тянется, и заканчивая конфликтами имен из разных библиотек. Почему не сделали так? (мне кажется это логичнее)

    let v = std::vec![1, 2, 3];
    


    2. Зачем нужно отдельно указывать extern_crate, почему просто не писать
    use derive_new::new;
    

    И если нет такого локального модуля, то воспринимать его как crate derive_new и качать зависимость.

    Может есть этому разумные обьяснения, кто знает?


    1. splav_asv
      15.02.2017 13:49
      +2

      На второй вопрос могу предложить почитать https://withoutboats.github.io/blog/rust/2017/01/04/the-rust-module-system-is-too-confusing.html

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


      1. ozkriff
        15.02.2017 14:07
        +1

        можно и нужно проще

        Про "нужно" не особо однозначно, а реддите хорошое обуждение у статьи было:


        https://www.reddit.com/r/rust/comments/5m4w95/the_rust_module_system_is_too_confusing/


        1. splav_asv
          15.02.2017 14:17
          +1

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


    1. ozkriff
      15.02.2017 14:12
      +3

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


      1. VadimVP
        15.02.2017 15:57
        +3

        План такой, что разрешение имен и разворачивание макросов будут работать вместе — "разрезолвили имена -> развенули макросы -> разрезолвили опять -> опять развернули" и так далее до точки равновесия.
        Оно на самом деле уже так частично работает, просто работа не закончена еще.


    1. VadimVP
      15.02.2017 15:22
      +5

      Те макросы, которые есть сейчас (macro_rules!) это в некотором смысле временная мера, связанная с тем, что в 1.0 должны были быть макросы, но привести их в идеальное состояние не было времени.


      Сейчас вместе с процедурными макросами, описанными в статье, делаются макросы by-example 2.0, у которых будет и собственное пространство имён, и модулярность, и разрешение имён совмещённое вместе с разворачиванием макросов в один этап.
      Это тоже дело долгое, но в roadmap'е назначено на этот год.


      1. dimack
        15.02.2017 19:17

        Вот это уже круть.


  1. mgrigorov_ua
    15.02.2017 16:01

    Rust готов к кровавому enterprise? Или рано ещё?


    1. Halt
      15.02.2017 16:30
      +4

      Enterprise бывает разный.

      Если просто как одно из средств разработки то да, можно. Базовые возможности языка стабилизированы начиная с версии 1.0. Этим гарантируется последующая обратная совместимость и что API внезапно не поменяется.

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

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

      Если говорить про веб, то лучше всего на этот вопрос ответит сайт arewewebyet.org.


      1. mgrigorov_ua
        15.02.2017 17:17

        Halt,

        спасибо за информацию. А как насчёт дебагера в Rust?


        1. Halt
          15.02.2017 17:22
          +2

          gdb, lldb, все как обычно.


        1. snuk182
          15.02.2017 17:38
          +2

          Eclipse + RustDT + GDB, VsCode + Rust + GDB. Насчет LLDB дебаггера пока информации не видел.


          1. grossws
            15.02.2017 19:19
            +2

            Есть, но там с отображением некоторых "встроенных" типов (Option, Result) было похуже, см. https://michaelwoerister.github.io/2015/03/27/rust-xxdb.html, но ситуация могла изменится, смотрел в середине 2016 года.


            1. snuk182
              15.02.2017 19:27
              +2

              В GDB отображение содержимого тоже не ахти, особенно для хитро вложенных по ссылкам членов структур, или массивов, или некоторых объектов на куче. Но в большинстве случаев юзабельно.