Использование комбинаторов and_then и map для типа результата Rust

Перевод | Автор оригинала: Herman J. Radtke III

Если вы потратили какое-то время на изучение Rust, вы быстро привыкнете к типам Option и Result. Именно благодаря этим двум основным типам мы делаем наши программы надежными. Мой опыт работы с C и динамическими языками. Я обнаружил, что при работе с этими типами проще всего использовать ключевое слово соответствия. Существуют также функции комбинатора, такие как map и and_then, которые позволяют объединить набор вычислений в цепочку. Мне нравится объединять комбинаторы в цепочку, чтобы логика ошибок была отделена от основной логики кода.

Я недавно вернулся домой с RustConf 2016, где в крэйте футур была версия 0.1.1 и первые проблески токио. Все футуры реализуют функцию опроса, которая возвращает тип опроса. Тип опроса определяется как тип публикации Poll <T, E> = Result<Async, E> ;. Таким образом, если мы хотим использовать футуры, нам нужно хорошо разбираться в функциях комбинатора, реализованных в основном типе Result. Вы не сможете отказаться от использования ключевого слова match. Многие из примеров, которые я видел, использовали функции комбинатора для объединения футур вместе. Мы можем посмотреть, как комбинаторы and_then и map работают с типом Result, и лучше понять, как работают комбинаторы, без дополнительной умственной нагрузки, связанной с попытками понять, как работают футуры. Когда мы освоимся с комбинаторами, мы сможем лучше понять примеры, в которых комбинаторы используются для объединения футур. (Edit: исправлено предыдущее предложение в соответствии с обсуждением r / rust).

Подход

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

Справочник API стандартной библиотеки для комбинаторов результатов хорошо справляется с пояснениями и простыми примерами. Однако в большинстве примеров используется один и тот же тип для вариантов Ok и Err. Я думаю, это затрудняет понимание того, что происходит. Я буду использовать вариант Err(&'static str) в примерах, чтобы легко идентифицировать сообщения об ошибках. Если вас смущает «статическое время жизни», знайте, что & »static str означает жестко запрограммированный строковый литерал. Пример: let foo: &'static str = "Hello World!" ;.

and_then Combinator

Начнем с комбинаторной функции and_then. Комбинатор and_then - это функция, которая вызывает замыкание тогда и только тогда, когда вариант типа перечисления Result - Ok(T).

let res: Result<usize, &'static str> = Ok(5);
let value = res.and_then(|n: usize| Ok(n * 2));
assert_eq!(Ok(10), value);

В этом первом примере значение res равно Ok(5). Согласно нашему определению and_then: and_then будет соответствовать варианту Ok и вызывать закрытие со значением usize 5 в качестве аргумента. Что произойдет, если res - вариант Err?

let res: Result<usize, &'static str> = Err("error");
let value = res.and_then(|n: usize| Ok(n * 2)); // <--- closure is not called
assert_eq!(Err("error"), value);```

В этом втором примере значение res равно Err(«ошибка»). Согласно нашему определению and_then: and_then будет соответствовать варианту Err и пропустить вызов закрытия. Значение Err(«ошибка») будет возвращено как есть. Это удобно, поскольку мы смогли написать замыкание, которое игнорировало ошибки. Значение Err(«ошибка») будет передано в фоновом режиме в конец цепочки комбинатора. Пока мы только возвращали Ок из закрытия. Наше закрытие также может вернуть ошибку.

Объединение нескольких функций and_then

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

let res: Result<usize, &'static str> = Ok(0);

let value = res
    .and_then(|n: usize| {
        if n == 0 {
            Err("cannot divide by zero")
        } else {
            Ok(n)
        }
    })
    .and_then(|n: usize| Ok(2 / n)); // <--- closure is not called

assert_eq!(Err("cannot divide by zero"), value);

Начальное значение Ok(0) будет передано первому закрытию. В этом случае n действительно равно 0, и замыкание возвращает Err(«не может делиться на ноль»). Наш следующий вызов and_then указывает, что теперь у нас есть вариант результата с ошибкой Err, и не вызывает закрытие.

Результаты сглаживания

Бывают случаи, когда у нас есть вложенные типы результатов. Как правило, это хорошая стратегия, чтобы попытаться сгладить результат. Например, мы можем преобразовать Result<Result<usize, &'static str>, &' static str> в Result<usize, &'static str>. Более плоский результат обычно легче обрабатывать более позднему коду.

let res: Result<Result<usize, &'static str>, &'static str> = Ok(Ok(5));

let value = res
	.and_then(|n: Result<usize, &'static str>| {
		n // <--- this is either Ok(usize) or Err(&'static str)
	})
	.and_then(|n: usize| {
		Ok(n * 2)
	});

assert_eq!(Ok(10), value);

В приведенном выше примере первое закрытие and_then возвращает n. Обратите внимание, что в предыдущих примерах мы заключили возвращаемое значение в вариант Ok или Err перечисления Result. В этом примере наша цель - сгладить результат, чтобы мы не возвращали явно Ok или Err. Значение n будет либо Ok(usize), либо Err(&'static str). Таким образом, мы можем вернуть n как есть. Если значение n имеет тип Ok(usize), то значение будет передано следующему and_then, как ожидалось. Если значение n имеет тип Err(&'static str), тогда вторая функция and_then будет пропущена.

Функция and_then в scala называется flatMap, и вы можете понять, почему. Мы сводим тип Result<Result<_, _>, > к Result<, _>, сопоставляя варианты во внутреннем Result с внешним Result.

map Combinator

До сих пор мы использовали and_then для объединения вычислений и сглаживания вложенных результатов. В примерах использовались результаты с типами, которые являются теми же типами, которые мы хотели получить в итоге. Иногда нам дают Результат, когда один или оба варианта не соответствуют тому типу, который нам нужен. Мы будем использовать карту для преобразования одного типа результата в другой.

Основы

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

let res: Vec<usize> = vec![5];
let value: Vec<usize> = res.iter().map(|n| n * 2).collect();
assert_eq!(vec![10], value);

Использование карты с типом результата немного отличается. Функция map вызывает закрытие тогда и только тогда, когда вариант перечисления Result равен Ok(T). Вот наш самый первый пример and_then, но вместо него используется карта.

let res: Result<usize, &'static str> = Ok(5);
let value: Result<usize, &'static str> = res.map(|n| n * 2);
assert_eq!(Ok(10), value);

Это очень похоже на первый пример and_then, но обратите внимание, что мы вернули Ok(n * 2) в примере and_then, а в этом примере мы возвращаем n * 2. Функция карты всегда оборачивает возвращаемое значение замыкания в вариант Ok.

Сопоставление варианта результата "ОК" с другим типом

Давайте посмотрим на пример, в котором вариант Ok(T) перечисления Result имеет неправильный тип. Пример: нам дан Result<i32, _>, но мы хотим Result<usize, _>.

let given: Result<i32, &'static str> = Ok(5i32);
let desired: Result<usize, &'static str> = given.map(|n: i32| n as usize);

assert_eq!(Ok(5usize), desired);

let value = desired.and_then(|n: usize| Ok(n * 2));

assert_eq!(Ok(10), value);

В этом примере значение res равно Ok(5i32). Согласно нашему определению map, map будет соответствовать варианту Ok и вызывать замыкание со значением i32, равным 5, в качестве аргумента. Когда замыкание возвращает значение, map обернет это значение в Ok и вернет его.

Если заданное значение является вариантом Err, оно передается через функции map и and_then без вызова замыкания.

let given: Result<i32, &'static str> = Err("an error");
let desired: Result<usize, &'static str> = given.map(|n: i32| n as usize); // <--- closure not called

assert_eq!(Err("an error"), desired);

let value = desired.and_then(|n: usize| Ok(n * 2)); // <--- closure not called

assert_eq!(Err("an error"), value);

Отображение обоих вариантов результата

Что, если бы оба варианта Результата были разными? Пример: нам дан Result<i32, MyError>, но мы хотим Result<usize, &'static str>.

Мы преобразуем только вариант Ok(i32) в приведенном выше примере. В этом примере нам также нужно будет преобразовать вариант Err(MyError) в Err(&'static str). Для этого нам нужно будет использовать map_err для обработки варианта Err(E). Комбинаторная функция map_err противоположна map, потому что она соответствует только вариантам Err(E) Result.

enum MyError { Bad };

let given: Result<i32, MyError> = Err(MyError::Bad);

let desired: Result<usize, &'static str> = given
    .map(|n: i32| {
       n as usize
    })
    .map_err(|_e: MyError| {
       "bad MyError"
    });

let value = desired.and_then(|n: usize| Ok(n * 2));

assert_eq!(Err("bad MyError"), value);

Вы должны понимать, что:

Различные типы возврата с использованием and_then And map

Функции and_then, map и map_err не обязаны возвращать один и тот же тип внутри своих вариантов. Функциям отображения можно дать Ok(T) и вернуть Ok(U). Функции map_err можно дать Err(E) и вернуть Err(F). Функции and_then можно дать Ok(T) и вернуть Ok(U) или Err(F)!

Давайте попробуем сложный пример, где нам дан вложенный результат, но ни один из типов не соответствует желаемым типам, которые мы хотим. Пример: нам дан Result<Result<i32, FooError>, BarError>, но нам нужен Result<usize, &'static str>.

enum FooError {
    Bad,
}

enum BarError {
    Horrible,
}

let res: Result<Result<i32, FooError>, BarError> = Ok(Err(FooError::Bad));

let value = res

    // `map` will only call the closure for `Ok(Result<i32, FooError>)`
    .map(|res: Result<i32, FooError>| {

        // transform `Ok(Result<i32, FooError>)` into `Ok(Result<usize, &'static str>)`
        res
            // transform i32 to usize
            .map(|n: i32| n as usize)

            // transform `FooError` into `'static str`
            .map_err(|_e: FooError| "bad FooError")

    })

    // `map_err` will only call the closure for `Err(BarError)`
    .map_err(|_e: BarError| {
        // transform `BarError` into `'static str`
        "horrible BarError"
    })

    // `and_then` will only call the closure for `Ok(Result<usize, &'static str>)`
    // Note: this is result of our first `map` above
    .and_then(|n: Result<usize, &'static str>| {
        // transform (flatten) `Ok(Result<usize, &'static str>)` into `Result<usize, &'static str>`
        // this may be `Ok(Ok(usize))` _or_ `Ok(Err(&'static str))`
        n
    })

    // `and_then` will only call the closure for `Ok(usize)`
    .and_then(|n: usize| {
        // transform Ok(usize) into Ok(usize * 2)
        Ok(n * 2)
    });

assert_eq!(Err("bad FooError"), value);
}

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

Вывод

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

Дополнительно

or_else Combinator

Комбинатор функций or_else противоположен and_then. Он вызывает закрытие только в том случае, если результат - Err(E). Я не использую or_else так часто, как and_then. Пожалуйста, не стесняйтесь показать мне, что мне не хватает.

Отладка сложных комбинаторов

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

// assume it is not clear what type `res` is
let res: Result<usize, &'static str> = Ok(5);
let c: u8 = res;

Что генерирует:

error[E0308]: mismatched types
 --> <anon>:5:13
  |
5 | let c: u8 = res;
  |             ^^^ expected u8, found enum `std::result::Result`
  |
  = note: expected type `u8`
  = note:    found type `std::result::Result<usize, &'static str>`

Обычно я использую переменную c, потому что хочу видеть тип res в сообщении об ошибке компилятора. Ха ха я знаю.

Вот пример его использования в комбинаторе:

let res: Result<usize, &'static str> = Ok(5);
let value = res.and_then(|wut| {
    let c: u8 = wut;
});

Что генерирует:

error[E0308]: mismatched types
 --> <anon>:6:17
  |
6 |     let c: u8 = wut;
  |                 ^^^ expected u8, found usize

error[E0308]: mismatched types
 --> <anon>:5:32
  |
5 | let value = res.and_then(|wut| {
  |                                ^ expected enum `std::result::Result`, found()
  |
  = note: expected type `std::result::Result<_, &str>`
  = note:    found type `()`

error: aborting due to 2 previous errors

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

Формат ночной ошибки

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

В вашем текущем рабочем каталоге: