Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Lidando com erros em Rust

Como retornar de funções em Rust valores e status.

Descrição

Na linguagem C encontramos muitas formas de retornar valores e status. Variáveis globais para o retorno de erro, passagem de ponteiro para estrutura ou tipo para que seja alterado, e então o retorno da função indica o que ocorreu, ou então a função retorna um ponteiro para um tipo ou estrutura e altera uma variável global de erro, e por aí vai.

Em linguagens como Ruby e Python, temos as exceções, então recebemos nossos retornos e nos protegemos capturando as possíveis exceções que possam ocorrer.

Em Rust temos um encapsulamento do status com o retorno. A função pode retornar um estrutura que indica sucesso, e nesse caso o retorno, e também se houve erro, e uma mensagem ou código, enfim, algum objeto.

O Livro, tem um longo capítulo dedicado ao tratamento de erros.

Antes de tratar erros, vamos ver alguns conceitos da linguagem que são usados na estratégia Rust para driblar erros.

Encontro de padrões

Temos esses exemplos em O livro:


let x = 5;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    4 => println!("four"),
    5 => println!("five"),
    _ => println!("something else"),
}

Pode-se colocar um bloco de código, entre {} para cada padrão. Esse é um exemplo de padrão literal, uma coisa mais direta. Observe que o padrão _ é o "pega-qualquer-coisa-que-sobrou".

Pode-se verificar mais de um padrão em um mesmo caso de teste, usando-se |:


let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

Agora, vamos a mais um elemento da linguagem, e então começa a diversão!

O tipo Result<>

Como dito acima, nada de variáveis globais, ponteiros e demais técnicas. A função retorna um pacote, que em caso de sucesso contém algum objeto de retorno, e em caso de falha um objeto para identificar a falha. Pode ser um número, um texto, um enum, qualquer coisa. Em minha memória, algo bem parecido com o union da linguagem C.

O tipo Result é definido dessa maneira na biblioteca padrão do Rust:


enum Result<T, E> {
   Ok(T),
   Err(E),
}

Quando preciso retornar de uma função posso, por exemplo, fazer isso:

Juntando tudo

Começa a ficar divertido quando testamos uma estrutura Result para verificar como a função chamada se comportou:


match some_function(some_argument) {
    Ok(value) => println!("got a value: {}", value),
    Err(_) => println!("an error occurred"),
}

O uso do _ é necessário para deixarmos claro que não nos interessa saber o que foi retornado em caso de erro, que interessa apenas saber que houve algum erro. Se for usado algum nome de variável, e não usá-la, o compilador irá gerar avisos (warnings).

Que tal usar enum?

Um importante componente da linguagem é o enum. Com ele podemos deixar o código bem mais legível e um pouco mais protegido de erros provenientes da confusão com códigos numéricos de erro.

Por exemplo, podemos definir alguns símbolos para poder saber o que realmente está acontecendo:


#[derive(Debug)]
#[derive(PartialEq)]
pub enum ProblemsError {
    InvalidOperator,
    DivideByZero,
}

Trata-se uma estrutura pública, portanto usa-se pub. Os símbolos definidos são ProblemsError, o nome de nosso enum, que contém os símbolos InvalidOperator e DivideByZero.

O #[derive(Debug)] permite que os símbolos tenham seus nomes impressos nas rotinas de debug, por exemplo, nos testes funcionais. Já o #[derive(PartialEq)] nos permite fazer comparação de igualdade com a estrutura criada.

Para referenciar os símbolos usamos, por exemplo, ProblemsError::DivideByZero.

Aqui um exemplo maior, tirado de meu projeto Test Driven Learning:


///  A função `operacao` deve receber dois parâmetros. O primeiro parâmetro é
///  um caractere indicando a operação aritmética básica a ser realizada ('+',
///  '-', '\*', '/'). O segundo parâmetro é um *array* de números inteiros, para
///  os quais a operação deve ser aplicada. A função deve retornar o resultado
///  da operação no *array*. Chame as funções já criadas para cada operação. Em
///  caso de operação inválida, gere uma exceção.
///
/// Examples
///
/// ```
/// extern crate rust;
///
/// assert_eq!(Some(3), rust::problems::operacao('+', vec![1, 2]).ok());
/// assert_eq!(Some(-1), rust::problems::operacao('-', vec![1, 2]).ok());
/// assert_eq!(Some(2), rust::problems::operacao('*', vec![1, 2]).ok());
/// assert_eq!(Some(0), rust::problems::operacao('/', vec![1, 2]).ok());
/// assert_eq!(Err(rust::problems::ProblemsError::InvalidOperator), rust::problems::operacao('=', vec![1, 2]));
pub fn operacao(operador: char, lista:Vec<i32>) -> Result<i32, ProblemsError> {
    match operador {
        '+' => Ok(soma(lista)),
        '-' => Ok(subtracao(lista)),
        '*' => Ok(multiplicacao(lista)),
        '/' => divisao(lista), // returns Result
        _   => Err(ProblemsError::InvalidOperator),
    }
}

Para entender os comentários com códigos, por favor veja o texto Testes funcionais em Rust.

Em minha função eu recebo um caractere com o símbolo da operação e traduzo para uma função que execute a tarefa. Usamos o match para encontrar o padrão, e nas operações em que não encontramos problemas, retornamos o resultado encapsulado por um Ok(). Exceção nesse caso é a função divisao, que pode retornar um erro se um dividendo for zero. Por fim, se é passado um operador não conhecido _, encapsulamos o código de erro com Err().

Observe-se também a declaração de retorno da função:


Result<i32, ProblemsError>

O tipo i32 designa um inteiro de 32 bits, e ProblemsError é o nosso enum.