← Свитки rust

Result и Option: Конвейер

Почему цепочка поисков выигрывает у отдельной проверки на каждом шаге

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

Конвейер

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

Привычка, которую ты только что усвоил

Как только ты научился уважать печать, тянет вскрывать каждую коробку сразу же, как получил. Нашёл книгу - открой через 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 - ничего не читается на веру, правило архива не гнётся - просто проверка идёт на линии, по клерку на шаг, а результат ты получаешь один раз, в конце, когда действительно пора решать. Две причины, по которым поручение могло вернуться пустым - нет книги, нет шифра полки, - по дороге схлопываются в одно сообщение; если позже понадобится их различать, для этого и нужен отдельный ? на каждом шаге.



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