Deixando código WIP como aviso em Rust
(blog.dureuill.net)- Durante o desenvolvimento em Rust, se você tentar resolver imediatamente todos os caminhos de erro e problemas de ownership, é fácil interromper o fluxo de implementação; por isso, é útil primeiro construir o happy path e acompanhar o trabalho restante com avisos
- Mesmo só com Rust padrão, é possível combinar
unwrap,clone,todo!, avisos padrão do compilador e o truque de comentários de documentação///para marcar código incompleto - As abordagens existentes têm limitações:
unwrapeclonenão geram avisos,todo!depende de avisos indiretos, e o truque com///pode causar erro de compilação ou documentação indesejada, dependendo da posição - O crate
wipexpõewip!,unwrap_wip,clone_wipefixme!para revelar implementações temporárias,unwrap/clonetemporários e lembretes de correção sempre como avisos - A Meilisearch usa
-D warningno CI para transformar avisos em erros e impedir a entrada de código WIP, removendo a dependência dewipadicionada durante o desenvolvimento no processo de limpeza do histórico antes da revisão
Por que avisos são necessários durante o desenvolvimento em Rust
- Rust é uma linguagem focada em correção, mas lidar com todos os caminhos de erro imediatamente durante o desenvolvimento pode reduzir a velocidade de implementação e a concentração
- Quando o
rustcbloqueia a compilação por causa de retornoResult, tratamento de erro ou implementação de casos de borda, normalmente surgem duas opções- Implementar imediatamente um tratamento de erro completo e refatorar várias vezes até preparar o PR
- Deixar uma marca do tipo
TODO later, reduzindo o problema ao nível de aviso e seguindo com o trabalho atual
- Avisos não bloqueiam o desenvolvimento, mas continuam como sinal de que algo precisa ser resolvido antes do merge do PR
- A Meilisearch executa o compilador no CI com a flag
-D warning, transformando todos os avisos em erros para evitar que código WIP seja publicado por engano - Code review é uma prática básica para evitar bugs e compartilhar conhecimento dentro do time, mas depender só da memória e da atenção das pessoas inevitavelmente leva a erros, então são necessários guardrails adicionais
Como adiar tratamento de código incompleto com Rust padrão
-
Adiar tratamento de erro com
unwrap- Ao usar
unwrapem umResultouOption, você pode extrair o valor interno e continuar a implementação atual em troca de um panic no caminho de erro - Depois que o restante do código estiver pronto, dá para identificar e remover os
unwrapadicionados temporariamente, substituindo-os por tratamento de erro adequado - Essa abordagem parte da ideia de que, embora Rust ofereça uma estrutura para lidar corretamente com erros, não é necessário concluir esse tratamento imediatamente
- Ao usar
-
Adiar problemas de ownership com
clone- Problemas de ownership são menos frequentes do que tratamento de erro, mas manter ownership corretamente pode exigir mudanças de design
- Nesses casos, em vez de mover o valor, é possível usar
clonepara evitar temporariamente problemas de lifetime - Mais tarde, é possível identificar empiricamente casos em que o
clonepode ser removido passando a variável adequada para um nível mais alto da stack - Ainda assim, como a correção pode exigir mudanças na estrutura do código, adiar esse tipo de ajuste tende a ser mais difícil do que adiar tratamento de erro
-
Reservar lugar para implementação com
todo!()- O macro
todo!()da biblioteca padrão do Rust indica código inacabado e é útil como placeholder para passar pela análise de tipos durante a prototipagem - Como
todo!()diverge com panic, ele pode ser usado como expressão placeholder na maioria das posições de tipo - Isso permite usar a definição de uma função enquanto a interface está sendo desenhada, deixando a implementação real para depois
todo!()é menos uma forma de informar ao usuário que um recurso não foi implementado e mais uma conveniência WIP para quem escreve e revisa o código- Para informar ao usuário que uma funcionalidade ainda não foi implementada, existe o macro
unimplemented!()
- O macro
-
Avisos padrão do compilador e o truque com comentários de documentação
- Os avisos padrão do compilador Rust também são úteis para mostrar estados incompletos durante o desenvolvimento
- bindings não usados
mutdesnecessárioResulteOptionnão usados
- Se durante o desenvolvimento esses avisos forem temporariamente suprimidos com prefixo
_ou outros truques, há o risco de o código incompleto passar despercebido - Casos de borda ou lógica quebrada que não são rastreados pelo sistema de tipos às vezes são deixados em comentários
- Se o comentário for escrito para parecer um comentário de documentação
///, o Rust emite um aviso para comentários de documentação sem item documentado, permitindo rastrear esse problema naquele ponto como aviso
- Os avisos padrão do compilador Rust também são úteis para mostrar estados incompletos durante o desenvolvimento
Limitações das abordagens padrão
- O maior problema é que nenhuma dessas técnicas gera de forma confiável um aviso diretamente ligado ao problema que precisa ser resolvido
todo!()frequentemente provoca avisos, mas em muitos casos eles vêm de causas indiretas, como argumentos de função não usados ou ausência de mutação necessáriaunwrapeclonenão geram aviso algum, então precisam ser removidos relendo o código antes do PR finalunwrappode perfeitamente permanecer em produção, então seu uso temporário para WIP pode se misturar com seu uso normal- O truque com comentário de documentação
///é próximo de um hack e, em posições de expressão, comentários de documentação são proibidos, podendo causar erro de compilação - Se
///for usado em um lugar que realmente gera documentação, o aviso esperado pode não aparecer e documentação indesejada pode acabar no código publicado
Ferramentas oferecidas pelo crate wip
- O crate
wipé um conjunto de ferramentas para tornar mais prática, durante o desenvolvimento em Rust, a estratégia de “avisar agora para resolver depois” - Ele pode ser adicionado como qualquer crate comum
cargo add wip
- Nos arquivos necessários, é possível importar o prelude
use wip::prelude::*;
- O
wipdepende dos avisos de depreciação do Rust, então pode ser menos incômodo desativar no editor o recurso que risca chamadas de funções deprecated -
wip::wip!- O macro
wip!funciona comotodo!, mas sempre gera um aviso quando usado - Ele pode ser usado nos lugares onde antes seria usado
todo!para marcar código incompleto - Nas versões iniciais o nome era
todo, mas o Rust não favorecia essa dependência ao tentar fazer shadow do macro destd, o que causava problemas no uso via prelude
- O macro
-
unwrap_wipeclone_wipunwrap_wipeclone_wipsão implementados como extension traits paraResulteOption- Quando estão no escopo, funcionam como
unwrapeclonenormais, mas geram aviso ao serem usados - Isso deixa claro que não há intenção de manter esse
unwrapoucloneno código final
-
wip::fixme!- O macro
fixme!é uma variação não panicante dewip!e não substitui expressões - Ele pode ser usado como um comentário TODO, mas gera avisos de forma estável sem recorrer ao truque com
/// - É frequentemente usado para deixar notas sobre correções que precisam ser feitas depois
- O macro
Fluxo de uso e cuidados
- O crate
wipfoi usado em alguns PRs recentes e também em um dos PRs grandes, Meilisearch PR #6484 - O fluxo típico é adicionar a dependência
wipao crate do workspace durante o desenvolvimento e removê-la ao reescrever o histórico antes da revisão - O
wipajuda a manter o foco no happy path sem esquecer o que precisa ser resolvido depois - Também há desvantagens
- Se você adicionar
fixmesem contexto suficiente, pode ser difícil corrigir depois - Em PRs grandes, a quantidade de avisos gerados por
wippode se tornar um peso
- Se você adicionar
- A API atual pode ser consultada na documentação do
wipno docs.rs
1 comentários
Comentários no Lobste.rs
Sinceramente, olhando a justificativa do
wip, fica a impressão de que reinventaram o Clippy por não entender como a ferramenta deveria ser usada originalmenteOs desenvolvedores do toolchain vêm distinguindo o que deve ser aviso do
rustce o que deve ser lint do Clippy, e owipparece recriar funcionalidades já existentes com outro nome para empurrar à força paracargo checkecargo buildlints que estavam nocargo clippyPor exemplo, a explicação de
wip!é basicamente fazer#![warn(clippy::todo)]aparecer fora docargo clippy, ao custo de adicionar uma nova dependência e sintaxe não padrão, eunwrap_wiptambém é parecido com#![warn(clippy::unwrap_used)]Para mim,
.unwrap()é para prototipagem, então ativo#![warn(clippy::unwrap_used)], e.expect("descrição da invariante")é para produção, onde eu usaria.unwrap()em vez de.unwrap_wip(), então não ativo#![warn(clippy::expect_used)]. Ex.:.expect("parse regex from const")Não tenho muita opinião sobre
clone_wipefixme!, e nunca pensei o bastante sobre esses casos para responder como os desenvolvedores pretendiam que fossem tratadosDito isso, é verdade que o texto não abordou a discussão sobre linters já existentes como o Clippy. Isso também tem a ver com meu uso pessoal: antes eu rodava o Clippy ao salvar, mas no nosso projeto ele ficou lento demais e passou a levar vários minutos entre salvamentos, então parei; hoje ele roda só no CI e em hooks antes do push
Trazer os avisos para o
rustcé muito mais rápido, e poder ver avisos inline durante o ciclo de iteração é uma parte central desse fluxo de trabalhoA configuração do Clippy também era bem incômoda, pelo menos da última vez que verifiquei. Não havia um jeito de definir lints em código de uma vez para todos os crates do workspace, então, para cada lint que queríamos rejeitar no workspace, era preciso adicionar um
-Dexplícito no CI ou continuar colocando atributos globais em todos os crates#![warn(clippy::todo)]seria necessário em todos os crates, então isso não traz tanta vantagem para nós quanto adicionar e remover crates só durante o desenvolvimento. Acho quetodo!deveria gerar aviso por padrão norustcou no Clippy, e concordo que, se isso acontecesse, a utilidade dowipdiminuiriaTambém não concordo que
unwrap_wipseja equivalente a#![warn(clippy::unwrap_used)], por dois motivos. Primeiro, em termos de intenção, a distinção entreunwrapcomo não produção eexpectcomo produção é comum, mas não universal. Segundo, a base de código existente tem cerca de 4.600 unwraps, e até daria para trocar todos porexpect, mas isso consumiria tempo demais inserindo mensagens temporárias por toda parte ou reconstruindo pré-condições como “este mutex não sofreu poison” e “quero propagar a falha desta tarefa para o chamador”fixme!é muito útil, e eu adoraria vê-lo na biblioteca padrão ou algo do tipo. Na prática, é o recurso que mais usamos desse crateO texto não entrou nesses detalhes, mas, por esse crate ser uma biblioteca especializada e não padrão, ele também tem coisas como
wip_iter. Isso permite usar macros comotodoem funções comimpl Traitna posição de retorno para iteradores, ewip_futurefaz algo parecido. Já tive vários casos em que inferência de tipo de retorno etodonão funcionavam juntas, e eu precisava forçar tipos artificialmente. Alguém também sugeriu gerar erro de compilação por padrão no modo release, e pretendo analisar isso junto com outras ideias de melhoriaPara equilibrar um pouco o custo de nova dependência e sintaxe não padrão, isso é uma dependência de arquivo único usada só durante o desenvolvimento e normalmente removida no fim
O problema de prototipar com
unwrapé que, ao introduzir tratamento de erro de verdade depois, o formato do tipo de retorno pode mudar, e isso gera efeitos em cascata, especialmente se essas funções forem usadas em estilo funcionalEu recomendaria começar com
Result, usando tipo de erro comoBox<dyn Error>, string,anyhowetc. Depois dá para trocar por uma enumeração de erro real, mas é mais difícil refatorar mais tarde um código que parece não falhar para passar a retornarResultAlém disso, se você usa LLM, é bom ter cuidado. LLM é uma máquina de casar padrões, então não consegue distinguir “fiz isso de propósito de forma relaxada e quero corrigir depois” de “é assim que este projeto deve tratar erros”. Com o tempo, pode multiplicar decisões de “só por enquanto”, então ajuda deixar um ou dois comentários no main orientando onde/quando esse tratamento de erro mais frouxo é aceitável
Minha interpretação de
todo!()eunimplemented!()não bate com a da documentação. Ainda assim, o parágrafo seguinte que foi omitido compensa um pouco essa posiçãoA documentação diz que
todo!comunica a intenção de implementar depois e usa a mensagem “not yet implemented”, enquantounimplemented!não faz essa promessa e usa a mensagem “not implemented”Quando
todo!foi adicionado na 1.40.0, ele era pura e simplesmente um nome mais curto, sem diferença em relação aunimplemented!além do nomeSó na 1.42.0 o “yet” foi removido da mensagem de
unimplemented. Pelo que lembro, definir esse significado entre as duas macros também foi algo um pouco controverso, assim como lançartodo!como uma forma abreviada deunimplemented!Hoje em dia,
unimplemented!()é recomendado para casos como implementações de trait que precisam fornecer um corpo de método por razões técnicas, mas nunca deveriam ser chamadas. O exemplo da documentação é exatamente desse tipoFiquei pensando se esse fluxo de adicionar a dependência
wipa um crate do workspace durante o desenvolvimento e depois removê-la reescrevendo o histórico antes da revisão poderia ficar mais suave com alguma ferramenta extraTer que até reescrever o lockfile enquanto ajusta o meio de um PR me parece um pouco desagradável. Se nesse intervalo a árvore de dependências mudou em outro ponto, podem surgir conflitos desnecessários. Talvez dê para abusar do
jj fix. De qualquer forma, a ideia do projeto é muito boa e acho que vou acabar usando em algum lugarMacros de anotação que não causam panic, como
wip::fixme!, são uma ideia realmente boa. O truque com comentário de documentação também é um método elegante que vale a pena guardarHá ideias interessantes aqui que eu gostaria de ver levarem a mudanças no lado do
rustc.todo!()realmente deveria gerar aviso por padrãoSeria ainda melhor se o desenvolvedor pudesse desligar esses avisos quando estivesse em modo de hacking. Avisos demais acabam sendo sufocantes, e normalmente eu rodo
cargoem um console de 19 linhas na parte de baixo da IDE. Durante o desenvolvimento, os avisos acabam escondendo erros reais e avisos mais importantes. Só uma disciplina de sempre ordenar os erros por último já ajudaria a aliviar issoAlém disso, os nomes
.unwrap_wip()e.clone_wip()são compridos demais por serem maiores que os nomes originais. Eu sugeriria.du()e.dc()como abreviações de “dev unwrap” e “dev clone”. Isso ficaria mais prático para programação rápidaSerá que não daria para adicionar
.clone()e.unwrap()automaticamente onde for necessário? Por exemplo, com uma flag ou recurso como#[wip(autoclone,autounwrap)]. Provavelmente não, mas sonhar não custa nadaSempre que começo um projeto novo ou protótipo, coloco as linhas abaixo no
main.rs:#![allow(dead_code)]#![allow(unused_variables)]#![allow(unreachable_code)]Isso reduz bastante o ruído nas fases iniciais