1 pontos por GN⁺ 5 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 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: unwrap e clone nã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 wip expõe wip!, unwrap_wip, clone_wip e fixme! para revelar implementações temporárias, unwrap/clone temporários e lembretes de correção sempre como avisos
  • A Meilisearch usa -D warning no CI para transformar avisos em erros e impedir a entrada de código WIP, removendo a dependência de wip adicionada 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 rustc bloqueia a compilação por causa de retorno Result, 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 unwrap em um Result ou Option, 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 unwrap adicionados 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
  • 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 clone para evitar temporariamente problemas de lifetime
    • Mais tarde, é possível identificar empiricamente casos em que o clone pode 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!()
  • 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
      • mut desnecessário
      • Result e Option nã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

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ária
  • unwrap e clone não geram aviso algum, então precisam ser removidos relendo o código antes do PR final
  • unwrap pode 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 wip depende 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 como todo!, 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 de std, o que causava problemas no uso via prelude
  • unwrap_wip e clone_wip

    • unwrap_wip e clone_wip são implementados como extension traits para Result e Option
    • Quando estão no escopo, funcionam como unwrap e clone normais, mas geram aviso ao serem usados
    • Isso deixa claro que não há intenção de manter esse unwrap ou clone no código final
  • wip::fixme!

    • O macro fixme! é uma variação não panicante de wip! 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

Fluxo de uso e cuidados

  • O crate wip foi usado em alguns PRs recentes e também em um dos PRs grandes, Meilisearch PR #6484
  • O fluxo típico é adicionar a dependência wip ao crate do workspace durante o desenvolvimento e removê-la ao reescrever o histórico antes da revisão
  • O wip ajuda a manter o foco no happy path sem esquecer o que precisa ser resolvido depois
  • Também há desvantagens
    • Se você adicionar fixme sem contexto suficiente, pode ser difícil corrigir depois
    • Em PRs grandes, a quantidade de avisos gerados por wip pode se tornar um peso
  • A API atual pode ser consultada na documentação do wip no docs.rs

1 comentários

 
GN⁺ 5 시간 전
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 originalmente
    Os desenvolvedores do toolchain vêm distinguindo o que deve ser aviso do rustc e o que deve ser lint do Clippy, e o wip parece recriar funcionalidades já existentes com outro nome para empurrar à força para cargo check e cargo build lints que estavam no cargo clippy
    Por exemplo, a explicação de wip! é basicamente fazer #![warn(clippy::todo)] aparecer fora do cargo clippy, ao custo de adicionar uma nova dependência e sintaxe não padrão, e unwrap_wip també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_wip e fixme!, e nunca pensei o bastante sobre esses casos para responder como os desenvolvedores pretendiam que fossem tratados

    • Pode até ser uma fala sincera, mas não é uma forma muito gentil de colocar isso :-) Uso essa área há 10 anos e, claro, isso não significa que eu saiba tudo, mas acho que ainda dá para fazer uma interpretação de boa-fé
      Dito 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 trabalho
      A 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 -D explí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 que todo! deveria gerar aviso por padrão no rustc ou no Clippy, e concordo que, se isso acontecesse, a utilidade do wip diminuiria
      Também não concordo que unwrap_wip seja equivalente a #![warn(clippy::unwrap_used)], por dois motivos. Primeiro, em termos de intenção, a distinção entre unwrap como não produção e expect como 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 por expect, 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 crate
      O 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 como todo em funções com impl Trait na posição de retorno para iteradores, e wip_future faz algo parecido. Já tive vários casos em que inferência de tipo de retorno e todo nã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 melhoria
      Para 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 funcional
    Eu recomendaria começar com Result, usando tipo de erro como Box<dyn Error>, string, anyhow etc. 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 retornar Result
    Alé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!() e unimplemented!() não bate com a da documentação. Ainda assim, o parágrafo seguinte que foi omitido compensa um pouco essa posição
    A documentação diz que todo! comunica a intenção de implementar depois e usa a mensagem “not yet implemented”, enquanto unimplemented! 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 a unimplemented! além do nome
    Só 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çar todo! como uma forma abreviada de unimplemented!
    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 tipo

  • Fiquei pensando se esse fluxo de adicionar a dependência wip a 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 extra
    Ter 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 lugar

  • Macros 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 guardar

  • Há ideias interessantes aqui que eu gostaria de ver levarem a mudanças no lado do rustc. todo!() realmente deveria gerar aviso por padrão
    Seria ainda melhor se o desenvolvedor pudesse desligar esses avisos quando estivesse em modo de hacking. Avisos demais acabam sendo sufocantes, e normalmente eu rodo cargo em 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 isso
    Alé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ápida

  • Será 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 nada

  • Sempre 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