#rust
Есть примерно вот такой кусок кода парсера сайта с использованием select:
extern crate select;
use select::document::Document;
use select::predicate::{Class, Name, And};
fn main() {
// Пример; реальность чуть сложнее, но не суть
let html = "
";
let page = Document::from(html);
let blog_id: u32 = page.find(And(Name("div"),Class("vote-item")))
.find(Name("span")).first()
.unwrap().attr("id")
.unwrap().split('_').collect::>().last()
.unwrap().parse::().unwrap();
println!("Blog id: {}", blog_id);
}
Я попытался его переписать так, чтобы на выходе получить Option (который потом переведу
в Result; конкретно здесь сохранение точной причины ошибки не интересует). Результат
работает, но выглядит, мягко говоря, не очень:
let blog_id: Option = page.find(And(Name("div"),Class("vote-item"))).first()
.and_then(|x| x.find(Name("span")).first())
.and_then(|x| x.attr("id").and_then(|x| Some(x.to_string())))
.and_then(|x| x.split('_').collect::>().last().and_then(|x| Some(x.to_string())))
.and_then(|x| x.parse::().ok());
match blog_id {
Some(i) => println!("Blog id: {}", i),
None => println!("Cannot parse blog_id"),
};
Возможно ли оформить это как-нибудь более красиво? Если я пытаюсь это как-нибудь
упрощать (особенно противный to_string), компилятор незамедлительно начинает ругаться
на времена жизни, заимствования и прочую дребедень. Использование match или if let,
подозреваю, приведёт к зашкаливающему количеству лесенок, а если их избегать, то красивость
всё равно не особо увеличится. Ещё я пытался написать макрос с циклом, эквивалентный
постоянным вызовам and_then, но получилось так же некрасиво и to_string никуда не делись.
В идеале что-нибудь похоже на такой Python-эквивалент:
try:
blog_id = int(page.find("div", {"class": "vote-item"}).\
find("span")[0].\
get("id").\
rsplit("_")[-1])
except Exception:
blog_id = None
(наверно, лучше просто взять xpath, но вопрос пока не про это :)
Ответы
Ответ 1
Если в результате (в возвращаемом значении функции) нужно получить Result, для улучшения
читабельности кода можно воспользоваться макросом try!. Любое значение Option, полученное
из операций библиотеки select, можно преобразовать в Result методом ok_or, с типом
ошибки на ваш выбор, но таким, чтобы он преобразовывался (трейтами Into/From) в тип
ошибки для Result, возвращаемого функцией, которая содержит ваш код с использованием try!.
fn get_blog_id(page: &Document) -> Result {
let item = try!(page.find(And(Name("div"),Class("vote-item"))).first()
.ok_or(MyDocError::NoVoteItem)));
let id = try!(item.attr("id")
.ok_or(MyDocError::NoIdAttr)));
...
}
Мне непонятно, в чем проблема со временем жизни заимствований в цепочке and_then,
но изложенный выше подход позволит этого избежать, поскольку промежуточные значения
привязаны к слотам на время вызова содержащей функции и из них можно заимствовать отрезки
строк (AKA slices).
Возможный недостаток такого подхода в том, что значения-ошибки в параметрах вызовов
ok_or создаются в любом случае, так что если тип ошибки имеет нетривиальную инициализацию
и/или деструктор (например, создает String), это создает ненужную работу даже в случае
успешного выполнения. Чтобы этого избежать, можно воспользоваться ok_or_else и спрятать
инициализацию ошибки в лямбда-выражение, но это несколько ухудшит эстетическое качество
кода.
Добавлено: Библиотеки макросов try_or и try_opt позволяют написать код раннего возврата
Option или Result, подобный приведенному выше, более компактно. В последних версиях
языка использование стандартного макроса try! можно заменить на оператор ?.
Ответ 2
Начиная с Rust версии 1.22, оператором ? можно пользоваться и для быстрого возврата
Option::None:
fn get_blog_id(page: &Document) -> Option {
let item = page.find(And(Name("div"), Class("vote-item"))).first()?;
let id = item.attr("id")?;
// ...
}
Ответ 3
Сейчас как-то сошлись на использовании макроса hado, который похож на упомянутый
в комментариях mdo, но, по мнению некоторых, немножко лучше:
hado! {
el <- page.find(And(Name("div"),Class("vote-item"))).find(Name("span")).first();
id_s <- el.attr("id");
num_s <- id_s.split('_').last();
num_s.parse::().ok()
}
По сути он делает то же самое, что и мой код с and_then: для Some вычисляет следующее
выражение, а None оставляет как есть, и в результате отдаёт Option — но с более красивым
синтаксисом. (Проблем с временем жизни почему-то не возникло.) (Подобно mdo, юзает
многочисленные вложенные друг в друга Monad::bind, но пока каких-либо плохих последствий
этого не замечено.)
Ответ 4
Хм, unwrap говорите не нравится. Давайте посмотрим что мы имеем.
try!: Я давно запреметил один макрос, называется try!, суть его крайне проста. Если
есть ошибка то верни Result<..,..> на fn, если ошибки нет верни обьект. Принцип как
у unwrap.
macro_rules! try {
($e:expr) => (match $e {
Ok(val) => val,
Err(err) => return Err(err),
});
}
Описание: https://doc.rust-lang.org/std/macro.try.html
Код взят.
fn write_to_file_using_try() -> Result<(), MyError> {
//Result требуется обязательно реализовывать, так как вы используете try
let mut file = try!(File::create("my_best_friends.txt"));
//если все ок то верни обьект, если нет, заверши функцию и верни Result
try!(file.write_all(b"This is a list of my best friends."));
//если все ок то все ок, если нет, заверши функцию и верни Result
println!("I wrote to the file");
Ok(())
}
Также есть оператор ? не сталкивалось с ним работать. Prefer using ? syntax to try!.
? is built in to the language and is more succinct than try!. It is the standard method
for error propagation.
unwrap Думаю вы с ним уже познакомились, его код описывается зачастую так
fn unwrap(self) -> T {
match self {
Option::Some(val) => val, //если обьект относится к enum Option::Some(e)
значит он не null и достань значение из него и верни мне
Option::None => //если обьект относится к enum Option::None значит он
относится к нулу и значения никакого мы не получили
panic!("я упал"),
}
}
Есть еще множество способов описания, но они у меня не работают(бо неизведано кто
их реализовывает) http://m4rw3r.github.io/rust-questionmark-operator
Через IF (работает через что захочется, Option, Result, ...)
Перепишу код примера на IF
fn write_to_file_using_try() -> bool {
if let Ok(mut file) = File::create("my_best_friends.txt") {
if let Ok(_e) = file.write_all(b"This is a list of my best friends."){
println!("I wrote to the file");
return true;
}
}
false
}
Еще пример на IF из моего кода
pub fn save(&self) -> bool {
{
let lock = self.save_file.lock().unwrap();
if let Some(ref file_s) = *lock {
if let Ok(file_f) = File::create(file_s){
let mut file = BufWriter::new(file_f);
let end_w = [10u8; 1];
let locker = self.arr.lock().unwrap();
for i in locker.iter(){
file.write(i.as_bytes()).unwrap();
file.write(&end_w);
}
return true;
}
}
}
self.warning("no save file...");
false
}