Open source
Rust
Компиляторы
Программирование
Системное программирование
Комментарии 46
+5
Команда разработчиков Раста просто молодцы. Всегда с удовольствием читаю их посты о новых версиях.
В начале года они пообещали, что этот год пройдёт под эгидой упрощения разработки и снижения порога входа, и они держат свои обещания.
+4
Могу посоветовать поглядывать в регулярно обновляющийся milestone predictions, дабы быть в курсе относительно планируемого времени интеграции фич.

Очень жаль, что impl Trait находится в неопределенном состоянии (на то имеются веские причины). А остальное вполне движется. Я уже несколько месяцев пользуюсь VSCode + RLS и оно «просто работает».

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

А с учетом того что все это теперь распространяется через rustup, то начать пользоваться можно в пару команд.
+1
Пытаюсь сейчас зайти на него после с++. Необычно, но нравится.
Кстати есть вопрос. Допустим у меня происходит парсинг html-кода, который должен вернуть какое-то число.

fn parse_page_count(document : kuchiki::NodeRef) -> i32
{
    let parent = document.select(".bottom_info #pagination p").unwrap().nth(0).unwrap();
    let firstBold = parent.as_node().select("b").unwrap().nth(1).unwrap();
    let result     = firstBold.as_node().as_text().unwrap().borrow().parse::<i32>();

    match result
    {
        Err(_)         => panic!("Failed to parse page count"),
        Ok(pagesCount) => pagesCount
    }
}


Как вообще правильно и принято в Rust оборачивать все это в проверкии и возвращение ошибки? Как использовать оператор "?" вместо кучи unwrap? Какой возвращаемый тип должен быть у функции (понимаю, что Result, но как объединить для всего).

Еще местами не понятны, когда в цепочке вызовов как выше компилятор бросается ошибкой, что переменная уже borrowed, поэтому приходится создавать временную переменную и использовать ее дальше в цепочке. Неясно почему компилятор сам не может этого сделать/понять. В общем после обычных языков первое время очень долго думаешь :)
0
Это поможет, если оборачиваемые методы возвращают одну и ту же сигнатуру Result, оно потом пробрасывается вверх. А в случае как на моем примере, там тип Err отличается.
+1
Посмотрите на фрагмент, приведенный ниже или на полный вариант в окрестности строки 152.

impl ServerState {
    pub fn parse(&mut self, message: &str) -> Result<(), &'static str> {
        for (k, v) in parse_table(&message) {
            match k {
                "frame" => self.frame = Rc::new(Frame::parse_from(v)?),

                "deaths" => self.deaths = v[1 .. v.len()-1] // strip quotes
                    .split(',')
                    .filter(|e| e.len() > 0) // a,,b
                    .map(|x| x.parse().map_err(|_| "parse error"))
                    .collect::<Result<_,_>>()?,

                // TODO
                _ => unimplemented!()
            }

        }

        Ok(())
    }

    // ...
}


Из функции я выбрасываю колхозную ошибку &'static str, ибо лень и прототип. Фукнции парсинга возвращают нормальные ошибки, которые надо привести к строке.

Самое интересное происходит в обработке «deaths»: с помощью функциональной магии и трансформации монад мы преобразуем Collection<Result<T, E>> в Result<Collection, E>>, который потом разворачиваем с помощью ?.
+3
Если опустить детали реализации, то `?` просто выбрасывает ошибку из функции. Нормальное же значение «проглатывается» на текущем лексическом уровне. Подробнее можно посмотреть в документации.

Если у вас есть операция, которая возвращает к примеру io::Result, а ваша функция определяет свой тип ошибки, то к вашим услугам метод map_err().
+6
Пример из жизни:
fn create_player(player: &Player, tattva: Alb<Tattva>, storage: Al<Storage<Player, PlayerParams>>, idf: Al<IdFactory>) -> Result<Player, PlayerInitializeError> {
	let universe = get_universe(player.uid)?; // <- get_universe возвращает Result<Universe, PlayerInitializeError>. Если она вернула error, create_player тоже его возвращает тут же. Похоже на проброс исключения в Java.
	let mut player = player.clone();
	player.id = idf.write().unwrap().player();	
	let params = PlayerParams {
		universe: universe.name.clone(),
		lookup: PlayerParamsLookup::Id(player.id),
	};	
	storage.write().unwrap().save(&player, ¶ms).map_err(|e| {
		error!("[{}] Error saving player {} '{}': {}", SCENE_NAME, player.id, player.name, e);
		PlayerInitializeError::CannotSave
	})?; // save возвращает Result<(), StorageError>. StorageError нельзя вернуть вместо PlayerInitializeError, поэтому мы его преобразовываем с помощью map_err, а заодно и логгируем. Можно враппер написать, но обычно незачем, если все логгируется.	
	Ok(player)
}
+1
Спасибо, а вопрос по поводу цепочки вызовов?
Вот пока не совсем понимаю где можно использовать clone, где вот эти into. Даже в вашем примере, насколько правильно создавать копию игрока.
А так крутой язык, всем советую попробовать. Сначала казался бесполезным и не стоящим своего изучения.
Но вот если разработчики еще собираются работать над упрощением порога входа, то должно быть вообще круто.
Им бы еще поработать в направлении веба, чтобы было удобно писать бекенд. Понимаю, что это не ниша языка изначально, но сейчас, например, переписываю для своего сайта с аудиокнигами парсеры на Rust, все это мультипоточно в виде демона, было бы удобно использовать одни и те же структуры парсеров и бекенда сайта.
+1
Мне скоро нужно будет заняться одним очень масштабным мероприятием, где буду реализовывать систему электронных денег, оплаты, входа и и т.д. Хочу попробовать сделать бекенд на расте. Вот здесь его стабильность и строгая типизация может очень помочь от глупых ошибок. На librocket и смотрел :)
0
По поводу первого вопроса чуть разверну:
let parent = document.select(".bottom_info #pagination p").unwrap().nth(0).unwrap();
let firstBold = parent.as_node().select("b").unwrap().nth(1).unwrap();
let result    = firstBold.as_node().as_text().unwrap().borrow().parse::<i32>();

Почему код выше работает, а если объединить в одну строчку:
let firstBold = parent.as_node().select("b").unwrap().nth(1).unwrap().as_node().as_text().unwrap().borrow().parse::<i32>();

То матерится вот так
+4

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

0
Предположу, что первая строка создает некий объект, а в процессе получения result его часть заимствуют. Таким образом в первом случае result корректен до того момента, пока parent не выйдет из области видимости. Для однострочника parent создается только как временный и разрушается по окончании данной строки. Получается не у кого заимствовать.
Формально rust прав. Если временный объект не доживает до конца области видимости, можно получить подвисший указатель и долго искать в чем плавающий баг.
0
Формально прав, а практически этот объект не умрет сам по себе, верно?
Получается, без знания сигнатур функций и их возвращаемых значений нереально писать код, который будет компилироваться с первого раза.
0
Компилятор может освободить память на стеке и использовать для следующего объекта. Да, в общем случае нельзя. Отчасти поэтому отказались от автоматического вывода сигнатур.
+3
P.S. cargo check иногда сильно экономит время, если еще не пользуетесь.
+1
Да, спасибо, стоит уже на хоткее билда :)
В общем случае понятно. Раст заставляет вас пострадать, при написании кода, зато в результате получаем гарантию, что не придется дебажить потом непонятные вылеты и проблемы с памятью.
+3

Да, и чем дальше в лес, чем крупнее проект, тем это качество всё важнее.
При этом попутно происходит ещё и обучение — почему так или иначе делать нельзя или просто плохо в других языках, у которых нет такого строгого контроля.
А с опытом эта «борьба» с компилятором проходит, начинаешь чётко представлять как всё будет работать, и эти ошибки из-за непонимания перестают отнимать время.
Кстати, очень рекомендовал бы сразу изучить вдоль и впоперек стандартную библиотеку — она минималистична и это не отнимет много времени, зато многие "тупиковые" вопросы, когда непонятно что делать, будут сняты.

+2
Блогпост по теме объясняющий проблему http://manishearth.github.io/blog/2017/04/13/prolonging-temporaries-in-rust/
+1
По ссылке есть малоизвестная интересная фича, по крайней мере я не знал.
let owned; //
let maybe_foo = if some_condition {
thing.get_ref()
} else {
owned = thing.get_owned();
owned.as_ref()
};
+1
Блог http://manishearth.github.io/blog/ в целом очень информативен. Крайне рекомендую к прочтению вот эти статьи

http://manishearth.github.io/blog/2015/05/17/the-problem-with-shared-mutability/
http://manishearth.github.io/blog/2015/05/27/wrapper-types-in-rust-choosing-your-guarantees/
http://manishearth.github.io/blog/2015/05/30/how-rust-achieves-thread-safety/
http://manishearth.github.io/blog/2017/04/13/prolonging-temporaries-in-rust/
http://manishearth.github.io/blog/2017/01/10/rust-tidbits-box-is-special/
0
Но rocket пока работает только на ночных сборках. В продакшен такое пускать не очень хочется. Хотя выглядит он очень вкусно.
0
А что именно Rocket требует из ночных сборок?
Если я правильно понял то он нуждается в плагинах для кодогенерации, да?
А вообще есть какие-то планы у Rust по стабилизации данных фич?
0
Да, много фич которые ещё непонятно когда и в каком виде попадут в stable.
+3
Clone — это клонирование. Явная копия объекта (неявная — это Copy, но от Copy на практике больше мороки, чем пользы). Может быть не так тривиальна, если клонируемый тип имеет поля-ссылки.
Into/From — паттерн Adapter из ООП, преобразует одни типы в другие, заодно является полу-сахаром для компилятора, если надо использовать (вернуть из функции, передать в функцию) объект, несовместимый с сигнатурой, но зато имеющий реализацию Into или From. Но тоже явно, методы из типажей надо вызывать вручную.
+1
Но да, для From и Into явное, прошу прощения, слегка невнимательно прочитал.
0

На самом деле уточню, что для ошибок в случае try! / ? — вызов .into() происходит «под капотом», что сильно упрощает жизнь, да и в целом, макросы немного нивелируют принцип явности.

+4
Добавлю еще немного про обработку ошибок. В книге хорошо описано, как правильно работать с ошибками. Там разобраны все типичные ситуации: как создавать собственный тип ошибок, как пробрасывать ошибки наверх и т.д.

При чтении можно мысленно заменять try!() на ?, смысл будет тот же.
0
Спасибо, я уже наловчился со всеми мапперами ошибок. Главное найти грань, чтобы из трех строк кода не полчилось 30 :)
+2
Так там наоборот из 30 получится 3. Реализация трейтов преобразования поможет автоматически захватывать ошибки разных типов вообще без map_err(). Просто пишете ?, а оно само разберется.
+2

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

+1
Начал разбираться с растом, и удивился и порадовался его продуманности во многих вещах — контроль времени жизни ссылок, невозможность использовать после move, функциональные возможности. Но очень расстраивает отсутствие корутин и N:M-параллелизма. Даже аналога обычного C++-ного std::async нет из коробки, да и futures какие-то странные. Мне кажется, что если бы это было, то rust мог бы серьезно побороться за нишу серверныз приложений, а пока шансов, увы, немного.
Насколько я понял, была годная реализация корутин, но она оказалась несовместимой со стандартной библиотекой из-за того, что последняя активно использует TLS. И на данный момент корутины в принципе не в приоритете. Было бы хорошо, если бы кто-то внес уточнения, вдруг я что-то упустил.
0
Я в курсе, что такое std::async. И если быть точным, то она не всегда создает новый поток. Во-первых в зависимости от дефолтной политики результат может быть получен вообще синхронно. Во-вторых, в зависимости от имплементации асинхронная версия может либо создавать новый поток, либо использовать пул потоков.
За наводку на https://tokio.rs/ — спасибо.
+1

Вот в этом и проблема, что смешали в кучу 2 разных инструмента, добавив столь любимую Комитетом горстку implementation-defined. Нет, даже 3. Потому что запуск в эксклюзивном потоке и на тред пуле — две большие разницы.

+1

Хотя нет, если http://en.cppreference.com/w/cpp/thread/async ничего не упускает — всегда или новый поток, или ленивое значение. Однако это всё равно крайне плохое решение. Во-первых, это таки смешивание двух совершенно разных вещей в одну кучу. Во-вторых, что гораздо хуже, выбор производится флагами (!), да к тому же если указаны оба флага, выбор implementation-defined (!!!). В общем грусть-печаль от такой стандартной библиотеки.

0
Да, похоже, что thread pool не используется. Я был сбит с толку этой темой. Причем похоже не используется как раз из-за потенциальных проблем с TLS.
Только полноправные пользователи могут оставлять комментарии. , пожалуйста.