Есть ли лучший функциональный способ обработки вектора с проверкой ошибок? - PullRequest
0 голосов
/ 26 января 2019

Я изучаю Rust и хотел бы знать, как я могу улучшить код ниже.

У меня есть вектор кортежей формы (u32, String). Значения u32 представляют номера строк, а String s - текст в соответствующих строках. Поскольку все значения String могут быть успешно проанализированы как целые числа, я хочу вернуть Ok<Vec<i32>>, содержащий только что проанализированные String значения, но если нет, я хочу вернуть ошибку какой-либо формы (просто Err<String> в пример ниже).

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

fn data_vals(sv: &Vec<(u32, String)>) -> Result<Vec<i32>, String> {
    sv.iter()
        .map(|s| s.1.parse::<i32>()
                    .map_err(|_e| "*** Invalid data.".to_string()))
        .collect()
}

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

Мне удалось сделать это с помощью следующего кода:

fn data_vals(sv: &Vec<(u32, String)>) -> Result<Vec<i32>, String> {
    sv.iter()
        .map(|s| (s.0, s.1.parse::<i32>()
                  .or_else(|e| {
                      eprintln!("ERROR: Invalid data value at line {}:  '{}'",
                                s.0, s.1);
                      Err(e)
                  })))
        .collect::<Vec<(u32, Result<i32, _>)>>() // Collect here to avoid short-circuit
        .iter()
        .map(|i| i.1
             .clone()
             .map_err(|_e| "*** Invalid data.".to_string()))
        .collect()
}

Это работает, но кажется довольно запутанным и громоздким - особенно набранный collect() в середине, чтобы избежать короткого замыкания, поэтому все ошибки выводятся на печать. Вызов clone() также раздражает, и я не совсем уверен, зачем он нужен - иначе компилятор говорит, что я перехожу из заимствованного контента, но я не совсем уверен, что перемещается. Есть ли способ сделать это более чисто? Или я должен вернуться к более процедурному стилю? Когда я попытался, я получил изменяемые переменные и флаг, указывающий на успех и неудачу, что выглядит менее элегантно:

fn data_vals(sv: &Vec<(u32, String)>) -> Result<Vec<i32>, String> {
    let mut datavals = Vec::new();
    let mut success = true;
    for s in sv {
        match s.1.parse::<i32>() {
            Ok(v) => datavals.push(v),
            Err(_e) => {
                eprintln!("ERROR: Invalid data value at line {}:  '{}'",
                          s.0, s.1);
                success = false;
            },
        }
    }
    if success {
        return Ok(datavals);
    } else {
        return Err("*** Invalid data.".to_string());
    }
}

Может кто-нибудь посоветовать мне лучший способ сделать это? Должен ли я придерживаться процедурного стиля здесь, и если так, это может быть улучшено? Или есть более чистый функциональный способ сделать это? Или смесь двух? Любой совет приветствуется.

Ответы [ 3 ]

0 голосов
/ 26 января 2019

Я думаю, вот для чего partition_map() от itertools:

use itertools::{Either, Itertools};

fn data_vals<'a>(sv: &[&'a str]) -> Result<Vec<i32>, Vec<(&'a str, std::num::ParseIntError)>> {
    let (successes, failures): (Vec<_>, Vec<_>) =
        sv.iter().partition_map(|s| match s.parse::<i32>() {
            Ok(v) => Either::Left(v),
            Err(e) => Either::Right((*s, e)),
        });
    if failures.len() != 0 {
        Err(failures)
    } else {
        Ok(successes)
    }
}

fn main() {
    let numbers = vec!["42", "aaaezrgggtht", "..4rez41eza", "55"];
    println!("{:#?}", data_vals(&numbers));
}
0 голосов
/ 26 января 2019

В чисто функциональном стиле вы должны избегать побочных эффектов. Ошибки печати - это побочный эффект. Предпочтительным стилем будет возвращение объекта стиля:

Result<Vec<i32>, Vec<String>>

и распечатайте список после возврата функции data_vals.

Итак, по сути, вы хотите, чтобы ваша обработка собирала список целых чисел и список строк:

fn data_vals(sv: &Vec<(u32, String)>) -> Result<Vec<i32>, Vec<String>> {
    let (ok, err): (Vec<_>, Vec<_>) = sv
        .iter()
        .map(|(i, s)| {
            s.parse()
                .map_err(|_e| format!("ERROR: Invalid data value at line {}: '{}'", i, s))
        })
        .partition(|e| e.is_ok());

    if err.len() > 0 {
        Err(err.iter().filter_map(|e| e.clone().err()).collect())
    } else {
        Ok(ok.iter().filter_map(|e| e.clone().ok()).collect())
    }
}

fn main() {
    let input = vec![(1, "0".to_string())];
    let r = data_vals(&input);
    assert_eq!(r, Ok(vec![0]));

    let input = vec![(1, "zzz".to_string())];
    let r = data_vals(&input);
    assert_eq!(r, Err(vec!["ERROR: Invalid data value at line 1: 'zzz'".to_string()]));
}

Playground Link

Используется partition, который не зависит от внешнего ящика.

0 голосов
/ 26 января 2019

Побочные эффекты (eprintln!) в адаптере итератора определенно не являются «функциональными». Вы должны накапливать и возвращать ошибки и позволять вызывающей стороне справляться с ними.

Я бы использовал здесь fold. Цель fold - сократить список до одного значения, начиная с начального значения и увеличивая результат с каждым элементом. Это «одно значение» вполне может быть списком. Здесь, однако, есть два возможных списка, которые мы можем захотеть вернуть: список i32, если все значения верны, или список ошибок, если есть какие-либо ошибки (я решил вернуть String s для ошибок здесь, для простоты.)

fn data_vals(sv: &[(u32, String)]) -> Result<Vec<i32>, Vec<String>> {
    sv.iter().fold(
        Ok(Vec::with_capacity(sv.len())),
        |acc, (line_number, data)| {
            let data = data
                .parse::<i32>()
                .map_err(|_| format!("Invalid data value at line {}:  '{}'", line_number, data));
            match (acc, data) {
                (Ok(mut acc_data), Ok(this_data)) => {
                    // No errors yet; push the parsed value to the values vector.
                    acc_data.push(this_data);
                    Ok(acc_data)
                }
                (Ok(..), Err(this_error)) => {
                    // First error: replace the accumulator with an `Err` containing the first error.
                    Err(vec![this_error])
                }
                (Err(acc_errors), Ok(..)) => {
                    // There have been errors, but this item is valid; ignore it.
                    Err(acc_errors)
                }
                (Err(mut acc_errors), Err(this_error)) => {
                    // One more error: push it to the error vector.
                    acc_errors.push(this_error);
                    Err(acc_errors)
                }
            }
        },
    )
}

fn main() {
    println!("{:?}", data_vals(&[]));
    println!("{:?}", data_vals(&[(1, "123".into())]));
    println!("{:?}", data_vals(&[(1, "123a".into())]));
    println!("{:?}", data_vals(&[(1, "123".into()), (2, "123a".into())]));
    println!("{:?}", data_vals(&[(1, "123a".into()), (2, "123".into())]));
    println!("{:?}", data_vals(&[(1, "123a".into()), (2, "123b".into())]));
}

Начальное значение - Ok(Vec::with_capacity(sv.len())) (это оптимизация, позволяющая избежать перераспределения вектора при добавлении к нему элементов; более простая версия будет Ok(vec![])). Если срез пустой, это будет результат fold; закрытие никогда не будет вызвано.

Для каждого элемента замыкание проверяет 1) были ли какие-либо ошибки до сих пор (указано значением аккумулятора Err) или нет, и 2) является ли текущий элемент действительным или нет. Я сопоставляю два значения Result одновременно (объединяя их в кортеж), чтобы обработать все 4 случая. Затем замыкание возвращает Ok, если ошибок пока нет (со всеми проанализированными значениями на данный момент) или Err, если ошибок пока нет (с каждым найденным недействительным значением).

Вы заметите, что я использовал метод push, чтобы добавить элемент в Vec. Строго говоря, это мутация, которая не считается «функциональной», но, поскольку мы перемещаем сюда Vec, мы знаем, что других ссылок на них нет, поэтому мы знаем, что не влияем на любое другое использование эти Vec с.

...