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.
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:
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 cratestd
. 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?