Ты уже вскрывал запечатанную коробку. Смотрел, что внутри, действовал по обстоятельствам, шёл дальше - и это было правильно. Но делай так на каждом шаге цепочки поисков, и строк на проверку коробок уйдёт больше, чем на само поручение.
Конвейер
Представь тот же архив - запечатанные коробки, архивариуса, который не отпустит тебя с невскрытой. Новым посетителям не рассказывают одного: есть служебное помещение, где коробки вообще не открывают руками. Коробка едет по ленте. Клерк сканирует её - печать не срывается - и если внутри книга, клерк делает с ней то, что положено на этом участке линии, и запечатывает новую коробку с результатом. Если сканер находит записку - клерк ничего не делает. Коробка едет дальше нетронутой, минуя всех оставшихся клерков, пока не доедет до того, кто ждёт в конце линии.
Привычка, которую ты только что усвоил
Как только ты научился уважать печать, тянет вскрывать каждую коробку сразу же, как получил. Нашёл
книгу - открой через match. Получил шифр полки - открой ещё раз. Разобрал в шифре номер - открой
третий раз. Каждый match сам по себе верен. Три подряд превращают трёхшаговое поручение в девять
строк, шесть из которых просто передают значение дальше или возвращают ту же жалобу, что уже была
написана на коробке.
struct Book {
id: u32,
shelf_code: Option<String>, // например "SHELF-104"
}
fn find_book(id: u32, archive: &[Book]) -> Option<&Book> {
archive.iter().find(|book| book.id == id)
}
fn parse_slot(raw: &str) -> Result<u32, String> {
raw.parse().map_err(|_| format!("bad shelf slot: {raw}"))
}
fn shelf_slot(id: u32, archive: &[Book]) -> Result<u32, String> {
let book = match find_book(id, archive) {
Some(b) => b,
None => return Err("no book with that id".to_string()),
};
let code = match &book.shelf_code {
Some(c) => c,
None => return Err("book has no shelf code yet".to_string()),
};
let raw = code.trim_start_matches("SHELF-");
match parse_slot(raw) {
Ok(slot) => Ok(slot),
Err(e) => Err(e), // открывает в третий раз, просто чтобы вернуть то, что уже нашёл
}
}
По ленте
Линия - главный маршрут архива. По ней едет почти каждая коробка в здании; открывать руками нужно только в тот момент, когда решение приходится принимать лично тебе.
Клерка, который работает только с книгой и никогда не превращает её в записку, Rust называет map.
Клерка, который берёт книгу и может сам вернуть записку - если его часть работы способна
провалиться - называет and_then. Когда нужно принять решение, применяешь ? - и лично
заглядываешь в коробку. Книга внутри - вынимаешь её и сам несёшь к началу следующего отрезка.
Записка внутри - не ждёшь, пока лента доедет до финальной точки, а сходишь прямо здесь и несёшь
записку тому, кто отправил тебя за поручением.
Выстрой клерков в цепочку - и решать вручную приходится не на каждом шаге, а только там, где черту проводишь сам.
fn shelf_slot(id: u32, archive: &[Book]) -> Result<u32, String> {
let raw = find_book(id, archive)
// продолжает путь, только если книга есть и в ней есть шифр полки
.and_then(|book| book.shelf_code.as_deref())
// всегда успешен, книгу в записку не превращает
.map(|code| code.trim_start_matches("SHELF-"))
// записка здесь обрывает поручение
.ok_or_else(|| "book has no shelf code yet".to_string())?;
parse_slot(raw)
}
Что меняется
Ты перестаёшь распечатывать каждый Option и Result на месте. Цепочка проверяет всё то же самое,
что и цепочка match - ничего не читается на веру, правило архива не гнётся - просто проверка идёт
на линии, по клерку на шаг, а результат ты получаешь один раз, в конце, когда действительно пора
решать. Две причины, по которым поручение могло вернуться пустым - нет книги, нет шифра полки, -
по дороге схлопываются в одно сообщение; если позже понадобится их различать, для этого и нужен
отдельный ? на каждом шаге.
map- если в коробке книга, преобразует её; если записка - пропускает её нетронутойand_then- передаёт книгу следующему клерку, чей сканер тоже может вернуть записку?- сам заглядываешь в коробку; если там записка, сразу несёшь её тому, кто отправил тебя за поручением
Ещё кое-что стоит знать: на линии работают и другие клерки - filter заглядывает внутрь книги и
решает, годится ли она, or_else подставляет свою книгу вместо пришедшей записки, и тому подобное.
Два клерка этой статьи - не весь цех, а тот минимум, с которого стоит начинать читать чужой код.