Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Refatorando um programa escrito em Rust — Primeira empreitada

Algumas coisas me chamaram a atenção em um recente texto, sobre como baixar arquivos remotos com Rust. Essas coisas despertaram uma imensa vontade de refatorar esse código.

Refatorando

A checagem de erros em Rust é muito interessante. Não são necessárias variáveis globais para explicar o erro, nem levantar exceções para indicar um erro, nem passar à função chamada variáveis que receberão o status. Basta retornar ou o erro ou o resultado, em uma estrutura Result. No texto abaixo tratei um pouco desse assunto:

Lidando com erros em Rust

Uma vantagem do Result é que podemos escrever nosso código sem verificar um erro. Se ocorrer um erro, então o Rust se encarrega de retornar o erro por você. DESDE QUE, a função que você usa e a sua função tenham o mesmo tipo de retorno, afinal a estrutura Result é bem maleável.

Caso as estruturas sejam idênticas, você pode usar a macro try!(). Algo como:


let resultado = try!(algo());

Ou agora o novo padrão:


let resultado = algo()?;

Ambas as formas realizam propagação de erros. Se algo der errado, o retorno de sua função será o retorno recebido. São equivalentes a escrever:


let resultado = match algo() {
   Err(e) => return Err(e),
   Ok(r) => resultado = r,
};

Muito mais prático e legível usar as macros, especialmente a mais nova versão.

Mas como falei, desde que sua função tenha o mesmo retorno. Uma outra opção é o unwrap(). Mas só use se não tem problema que seu programa aborte caso algo dê errado. Usa-se o unwrap() nos casos em que as funções retornem o tipo Option.

Então você deve decidir se usa Result ou Option. Aconselho usar Result. Mas aqui tem uma boa explicação sobre onde o Option é usado:

std::option - Rust API documentation for the Rust option mod in crate std. https://doc.rust-lang.org/std/option/index.html

O principal é que devemos conviver com essas duas estruturas.

E a dificuldade do código do texto sobre download de arquivos está nisso. O texto está aqui:

Rust Pills — Download de arquivos

O código final é esse:


//! Crawler — My own crawler in Rust!

extern crate hyper;         // biblioteca (crate) não padrão
use std::env;               // argumentos env::args
use std::io::{Read, Write}; // para IO de arquivos
use std::fs::File;          // para criar arquivos
use std::path::Path;        // configurar nome de arquivo
use std::thread;            // concorrência


const ROBOT_NAME:  &'static str  = "paulohrpinheiro-crawler";
const BUFFER_SIZE: usize = 512;


fn download_content(url: &str) -> Result<String, String> {
    // Somos um respeitável e conhecido bot
    let mut headers = hyper::header::Headers::new();
    headers.set(hyper::header::UserAgent(ROBOT_NAME.to_string()));

    // Pega cabeçalhos (e possivelmente algum dado já)
    let client = hyper::Client::new();
    let mut response;

    match client.get(url).headers(headers).send() {
        Err(error) => return Err(format!("{:?}", error)),
        Ok(res)    => response = res,
    }

    // Cria arquivo para salvar conteúdo
    let filename = Path::new(&url).file_name().unwrap();
    let mut localfile;

    match File::create(filename) {
        Err(error)     => return Err(format!("{:?}", error)),
        Ok(filehandle) => localfile = filehandle,
    }

    // pega conteúdo e salva em arquivo
    loop {
        let mut buffer = [0; BUFFER_SIZE];

        match response.read(&mut buffer) {
            Err(read_error) => return Err(format!("{:?}", read_error)),
            Ok(bytes_read)  => {
                if bytes_read == 0 {
                    // não tem mais o que ler
                    break;
                }
                // vamos tentar escrever o que pegamos
                match localfile.write(&buffer[0..bytes_read]) {
                    Err(write_error) => return Err(format!("{:?}", write_error)),
                    Ok(bytes_write)  => {
                        if bytes_write != bytes_read {
                            return Err("Error in write.".to_string());
                        }
                    },
                }
            },
        }
    }

    return Ok(String::from(filename.to_str().unwrap()));
}


fn main() {
    // Pega os argumentos, mas ignorando o primeiro
    // que é o nome do programa.
    let mut args = env::args();
    args.next();

    // vetor para as threads que serão criadas
    let mut workers = vec![];

    // Pega o conteúdo de cada URL
    for url in args {
        workers.push(thread::spawn(move || {
            print!("{} - ", url);

            match download_content(&url) {
                Err(error)   => println!("{:?}", error),
                Ok(filename) => println!("{:?}", filename),
            }

            print!("\n\n");
        }));
    }

    // espera cada thread acabar
    for worker in workers {
        let _ = worker.join();
    }
}

O código refatorado é esse:


//! Crawler — My own crawler in Rust!

extern crate hyper;         // biblioteca (crate) não padrão
use std::env;               // argumentos env::args
use std::io::{Read, Write}; // para IO de arquivos
use std::fs::File;          // para criar arquivos
use std::path::Path;        // configurar nome de arquivo
use std::thread;            // concorrência


const ROBOT_NAME:  &'static str  = "paulohrpinheiro-crawler";
const BUFFER_SIZE: usize = 512;


#[derive(Debug)]
enum DownloadError {
    CantGet,
    CantRead,
    CantWrite,
    CantCreate,
    InvalidName,
}


fn download_content(url: &str) -> Result<String, DownloadError> {
    // Somos um respeitável e conhecido bot
    let mut headers = hyper::header::Headers::new();
    headers.set(hyper::header::UserAgent(ROBOT_NAME.to_string()));

    // Pega cabeçalhos (e possivelmente algum dado já)
    let client = hyper::Client::new();
    let mut response = match     client.get(url).headers(headers).send() {
        Err(_) => return Err(DownloadError::CantGet),
        Ok(r)  => r,
    };

    let local_filename = filename(url)?;
    let mut localfile = create_localfile(&local_filename)?;

    // pega conteúdo e salva em arquivo
    loop {
        let mut buffer = [0; BUFFER_SIZE];

        // conseguimos ler?
        let bytes_read = match response.read(&mut buffer) {
            Err(_) => return Err(DownloadError::CantRead),
            Ok(b)  => b,
        };

        // não tem mais nada?
        if bytes_read == 0 {
            break;
        }

        // vamos tentar escrever o que pegamos
        let bytes_write = match localfile.write(&buffer[0..bytes_read]) {
            Err(_) => return Err(DownloadError::CantWrite),
            Ok(b)  => b,
        };

        // conseguiu escrever o que leu?
        if bytes_write != bytes_read {
            return Err(DownloadError::CantWrite);
        }
    }

    return Ok(local_filename);
}


fn filename(url: &str) -> Result<String, DownloadError> {
    match Path::new(&url).file_name() {
        None       => Err(DownloadError::InvalidName),
        Some(name) => {
            match name.to_str() {
                None    => Err(DownloadError::InvalidName),
                Some(r) => Ok(String::from(r)),
            }
        }
    }
}


fn create_localfile(name: &str) -> Result<File, DownloadError> {
    match File::create(&name) {
        Err(_) => Err(DownloadError::CantCreate),
        Ok(f)  => Ok(f),
    }
}


fn main() {
    // Pega os argumentos, mas ignorando o primeiro
    // que é o nome do programa.
    let mut args = env::args();
    args.next();

    // Vetor para as threads que serão criadas
    let mut workers = vec![];

    // Pega o conteúdo de cada URL
    for url in args {
        // Cria thread para cada URL
        workers.push(thread::spawn(move || {
            print!("{} - ", url);

            match download_content(&url) {
                Err(e) => println!("ERR: {:?}", e),
                Ok(f)  => println!("OK:  saved as {:?}", f),
            }

            print!("\n\n");
        }));
    }

    // Espera as threads terminarem suas tarefas
    for worker in workers {
        let _ = worker.join();
    }
}

Criando os códigos de erro


#[derive(Debug)]
enum DownloadError {
    CantGet,
    CantRead,
    CantWrite,
    CantCreate,
    InvalidName,
}

A primeira linha é um facilitador. Quando precisarmos imprimir o erro, ele imprime o símbolo definido, por exemplo, quando não é possível baixar algum conteúdo ou arquivo:


ERR: CantGet

No programa, esse é o trecho responsável por essa saída:


match download_content(&url) {
    Err(e) => println!("ERR: {:?}", e),
    Ok(f)  => println!("OK:  saved as {:?}", f),
}

Modificando o retorno da função download_content


fn download_content(url: &str) -> Result<String, DownloadError> {
...
}

Agora ela retorna um código de erro ao invés de uma String. Declarando e inicializando uma variável com match

Nesse caso, para iniciar o processo de transferência do arquivo:


let mut response = match client.get(url).headers(headers).send() {
    Err(_) => return Err(DownloadError::CantGet),
    Ok(r)  => r,
};

Aqui para verificar se conseguiu ler:


let bytes_read = match response.read(&mut buffer) {
    Err(_) => return Err(DownloadError::CantRead),
    Ok(b) => b,
};

E assim para verificar se conseguiu escrever:


let bytes_write = match localfile.write(&buffer[0..bytes_read]) {
    Err(_) => return Err(DownloadError::CantWrite),
    Ok(b) => b,
};

Observe o ‘;’ ao final.

Gosto de declarar as variáveis bem perto de onde as uso. Assim está bem perto :).

Usando macro para repasse de erros


let local_filename = filename(url)?;
let mut localfile = create_localfile(&local_filename)?;

Outras mudanças

Além das funções criadas, filename e create_localfile, responsáveis por, respectivamente, definir o nome local do conteúdo baixado, e por criar o arquivo localmente.

Bem mais legível o código, e isso só foi possível por terem sido criados códigos de erro e padronizado o fluxo pelo nosso programa. Próximas refatorações

Ao executar o programa com algumas URLs que levem aproximadamente o mesmo tempo para realizar a tarefa, a saída no console fica toda embaralhada. Afinal, estamos trabalhando com threads... Isso deve ser arrumado.

Devemos adicionar alguns testes e documentação .

E por fim, mas não menos importante, que tal usar cores para diferenciar erros de finalizações bem sucedidas?