HeadlinesBriefing favicon HeadlinesBriefing.com

Parse Don't Validate em Rust: Vetores Não Vazios

Hacker News •
×

Como muitos programadores, acho o artigo de Alexis King sobre Parse, don't validate fascinante, porque dá um nome a um padrão que parece familiar e importante - um que observei e usei no passado sem o nomear explicitamente. Este artigo é uma revisão do padrão "Parse, don't validate" aplicado à linguagem de programação Rust (o artigo original usa Haskell). Estava particularmente interessado em encontrar exemplos educativos deste padrão na biblioteca padrão de Rust e noutros projetos bem conhecidos. Sem repetir o artigo original (por favor, leia-o primeiro!), aqui está o seu cerne. Considere o venerável Vec; o seu primeiro método retorna Option<&T>. Porquê? Porque um vetor não é garantido a ter quaisquer elementos, então o que acontece se first for invocado num vetor vazio? Retornar um Option neste caso é idiomático em Rust [1], com um açúcar sintáctico conveniente para aceitar o resultado de funções que retornam Option e decidir o que fazer a seguir. Então qual é o problema? Imagine que temos uma função para ler alguns caminhos de configuração a partir de uma variável de ambiente, ao mesmo tempo que impomos a invariante de que a lista não pode estar vazia: use anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories: Vec<Path Buf> = value.split(',').map(str::trim).map(Path Buf::from).collect(); ensure!(!directories.is_empty(), "empty CONFIG_DIRS"); Ok(directories) } Até agora, tudo bem. Agora, vamos ver um uso típico desta função: fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; match config_dirs.first() { Some(cache_dir) => initialize_cache(cache_dir), None => unreachable!("already checked that CONFIG_DIRS is non-empty"); } Ok(()) } Uma vez que get_configuration_directories retorna um resultado bem-sucedido, estamos garantidos de que o vetor não está vazio. E, no entanto, se quisermos obter o primeiro elemento deste vetor, temos que usar o método first que retorna Option<&T>. Somos, portanto, forçados - mais uma vez - a lidar com um caso potencialmente vazio (onde a opção é None). Como afirma o artigo original, isto tem vários problemas em termos de clareza do código, implicações potenciais no desempenho e uma bomba-relógio se a invariante for alguma vez alterada em get_configuration_directories. O problema central é que Vec é fundamentalmente um tipo que pode estar vazio; podemos escrever um comentário "Isto não pode estar vazio, prometo!" em todo o código relevante, mas isso não é formalmente verificado por nada. Um tipo para um vetor "não vazio" A solução é aproveitar o sistema de tipos para impor uma invariante recém-estabelecida. Podemos usar um tipo separado para um "vetor que não pode estar vazio"; na verdade, tais tipos já existem em vários crates de Rust - por exemplo, nonempty: pub struct Non Empty<T> { pub head: T, pub tail: Vec<T>, } Este tipo não tem um construtor que permita "nenhum elemento"; o seu new toma um elemento, e o seu first retorna &T sem um Option: pub const fn new(e: T) -> Self { Self::singleton(e) } pub const fn singleton(head: T) -> Self { Non Empty { head, tail: Vec::new() } } pub const fn first(&self) -> &T { &self.head } O resto do crate preocupa-se em fazer com que Non Empty se comporte o mais próximo possível de um Vec normal, implementando muitos traits úteis, bem como conversões como: pub fn from_vec(mut vec: Vec<T>) -> Option<Non Empty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(Non Empty { head, tail: vec }) } } Vejamos como a nossa função get_configuration_directories se pareceria se retornasse um Non Empty em vez de um Vec simples: fn get_configuration_directories() -> Result<Non Empty<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(Path Buf::from).collect(); let Some(directories) = Non Empty::from_vec(directories) else { bail!("CONFIG_DIRS cannot be empty"); }; Ok(directories) } Note o uso de Non Empty::from_vec aqui - é aqui que a invariante é estabelecida. Agora...

Pergunta