1 points par GN⁺ 5 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Pendant le développement en Rust, traiter immédiatement tous les chemins d’erreur et les problèmes de possession peut facilement interrompre le flux d’implémentation ; une stratégie utile consiste donc à construire d’abord le happy path, puis à suivre le travail restant sous forme d’avertissements
  • Même avec Rust standard, on peut combiner unwrap, clone, todo!, les avertissements par défaut du compilateur et l’astuce des commentaires de documentation /// pour signaler du code inachevé
  • Les approches existantes ne font pas émettre d’avertissement par unwrap ni clone, todo! dépend aussi d’avertissements indirects, et l’astuce /// peut, selon l’emplacement, provoquer une erreur de compilation ou une documentation non voulue
  • La crate wip rend les implémentations temporaires, les unwrap/clone temporaires et les notes à corriger plus tard toujours visibles sous forme d’avertissements via wip!, unwrap_wip, clone_wip et fixme!
  • Meilisearch transforme les avertissements en erreurs dans la CI avec -D warning pour empêcher l’introduction de code WIP, et utilise un flux où la dépendance wip ajoutée pendant le développement est supprimée lors du nettoyage de l’historique avant review

Pourquoi des avertissements sont nécessaires pendant le développement en Rust

  • Rust est un langage qui met l’accent sur la correction, mais traiter tous les chemins d’erreur immédiatement pendant le développement peut nuire à la vitesse d’implémentation et à la concentration
  • Quand rustc bloque la compilation pour des raisons comme le retour d’un Result, la gestion des erreurs ou l’implémentation de cas limites, on se retrouve généralement face à deux choix
    • Implémenter tout de suite une gestion des erreurs complète, puis la refactorer plusieurs fois avant que la PR soit prête
    • Laisser un marqueur de type TODO later, abaisser le problème au niveau d’un avertissement et continuer le travail en cours
  • Un avertissement ne bloque pas le développement, tout en restant un signal à traiter avant la fusion de la PR
  • Meilisearch exécute le compilateur avec le flag -D warning dans la CI afin de transformer tous les avertissements en erreurs et d’éviter que du code WIP ne soit déployé par erreur
  • La revue de code est une pratique de base pour éviter les bugs et partager la connaissance dans l’équipe, mais s’appuyer uniquement sur la mémoire et l’attention humaines finit par entraîner des erreurs ; des garde-fous supplémentaires sont donc nécessaires

Repousser le traitement du code inachevé avec Rust standard

  • Repousser la gestion des erreurs avec unwrap

    • Utiliser unwrap sur un Result ou un Option permet d’extraire la valeur interne et de poursuivre l’implémentation en cours, au prix d’un panic sur le chemin d’erreur
    • Une fois le reste du code prêt, on peut repérer les unwrap ajoutés temporairement, les supprimer et les remplacer par une gestion d’erreur appropriée
    • Cette approche repose sur l’idée que, même si Rust fournit une structure pour gérer correctement les erreurs, il n’est pas nécessaire de terminer ce traitement immédiatement
  • Repousser les problèmes de possession avec clone

    • Les problèmes de possession sont moins fréquents que la gestion des erreurs, mais préserver une possession correcte peut nécessiter des changements de conception
    • Dans ce cas, on peut temporairement éviter les problèmes de durée de vie en clonant une valeur avec clone au lieu de la déplacer
    • Par expérience, on peut identifier plus tard les cas où le clone peut être supprimé en transmettant la variable appropriée plus haut dans la pile
    • La correction peut toutefois entraîner une modification de la structure du code, ce qui rend ce report plus délicat que pour la gestion des erreurs
  • Réserver l’emplacement d’une implémentation avec todo!()

    • La macro todo!() de la bibliothèque standard Rust indique du code inachevé et sert de placeholder utile pour passer l’analyse de types pendant le prototypage
    • Comme todo!() diverge avec un panic, elle peut être utilisée comme expression de substitution dans la plupart des emplacements typés
    • Elle permet d’utiliser des définitions de fonctions pendant que l’interface se précise, tout en repoussant l’implémentation réelle
    • todo!() ressemble davantage à une commodité WIP destinée aux auteurs du code et aux reviewers qu’à un moyen d’informer les utilisateurs du programme qu’une fonctionnalité n’est pas implémentée
    • Pour informer les utilisateurs qu’une fonctionnalité n’est pas implémentée, il existe une macro distincte, unimplemented!()
  • Avertissements par défaut du compilateur et astuce des commentaires de documentation

    • Les avertissements par défaut du compilateur Rust sont également utiles pour faire apparaître des états inachevés pendant le développement
      • Bindings inutilisés
      • mut inutile
      • Result et Option non utilisés
    • Si l’on supprime temporairement ces avertissements pendant le développement avec un préfixe _ ou d’autres astuces, du code inachevé risque de passer tel quel
    • Certains cas limites ou logiques cassées qui ne sont pas suivis par le système de types sont parfois laissés sous forme de commentaires
    • En écrivant un commentaire qui ressemble à un commentaire de documentation ///, Rust émet un avertissement lorsqu’il n’y a pas d’élément à documenter, ce qui permet de suivre le problème à cet endroit comme un avertissement

Limites des approches standard

  • Le principal problème est qu’aucune de ces techniques ne produit de manière fiable un avertissement lié au problème à résoudre
  • todo!() déclenche souvent des avertissements, mais ceux-ci proviennent fréquemment de causes indirectes, comme des paramètres de fonction inutilisés ou une mutation nécessaire qui n’a pas lieu
  • unwrap et clone n’émettent aucun avertissement, il faut donc relire le code avant la PR finale pour les supprimer
  • unwrap peut aussi rester légitimement en production, ce qui peut mélanger ses usages WIP temporaires et ses usages ordinaires
  • L’astuce du commentaire de documentation /// tient davantage du hack, et dans une position d’expression, les commentaires de documentation sont interdits et peuvent provoquer une erreur de compilation
  • Si /// est utilisé à un emplacement réellement documenté, l’avertissement attendu n’apparaît pas et une documentation non désirée peut rester dans le code livré

Les outils fournis par la crate wip

  • La crate wip est un ensemble d’outils qui vise à faciliter, pendant le développement Rust, la stratégie consistant à “laisser un avertissement à traiter plus tard”
  • Elle peut être ajoutée comme une crate classique
    • cargo add wip
  • Dans les fichiers nécessaires, on peut importer le prelude
    • use wip::prelude::*;
  • wip s’appuie sur les avertissements de dépréciation de Rust ; si l’éditeur barre les appels aux fonctions deprecated, il peut être moins gênant de désactiver cette fonctionnalité
  • wip::wip!

    • La macro wip! se comporte comme todo!, mais émet toujours un avertissement lorsqu’elle est utilisée
    • Elle peut être utilisée aux endroits où l’on aurait utilisé todo! pour signaler du code inachevé
    • Dans les premières versions, elle s’appelait todo, mais lorsque la dépendance tentait de masquer la macro std, Rust ne privilégiait pas ce comportement, ce qui posait problème avec l’utilisation du prelude
  • unwrap_wip et clone_wip

    • unwrap_wip et clone_wip sont implémentées comme des extension traits pour Result et Option
    • Lorsqu’elles sont dans le scope, elles se comportent comme unwrap et clone, mais émettent un avertissement à l’utilisation
    • Elles permettent de signaler clairement que l’intention n’est pas de laisser un unwrap ou un clone dans le code final
  • wip::fixme!

    • La macro fixme! est une variante non-panicking de wip! et ne remplace pas une expression
    • Elle peut être utilisée comme un commentaire TODO, mais génère un avertissement de façon fiable sans l’astuce ///
    • Elle sert souvent à laisser des notes sur ce qu’il faudra corriger plus tard

Flux d’utilisation et points d’attention

  • La crate wip a été utilisée dans plusieurs PR récentes, notamment dans l’une des grosses PR, Meilisearch PR #6484
  • Le flux habituel consiste à ajouter la dépendance wip à une crate du workspace pendant le développement, puis à la supprimer lors de la réécriture de l’historique avant la review
  • wip aide à rester concentré sur le happy path tout en évitant d’oublier le travail à traiter plus tard
  • Elle présente aussi des inconvénients
    • Si l’on n’écrit pas assez de contexte en ajoutant un fixme, il peut être difficile de le corriger plus tard
    • Dans les grosses PR, le nombre d’avertissements produits par wip peut devenir pesant
  • L’API actuelle est consultable dans la documentation wip sur docs.rs

1 commentaires

 
GN⁺ 5 시간 전
Commentaires sur Lobste.rs
  • Honnêtement, à voir la justification de wip, j’ai l’impression qu’ils ont réinventé Clippy faute de comprendre comment les outils sont censés être utilisés
    Les développeurs de la toolchain distinguent ce qui relève d’un avertissement rustc et ce qui relève d’un lint Clippy, mais wip semble recréer des fonctionnalités existantes sous un nouveau nom et déplacer de force vers cargo check et cargo build des lints qui se trouvaient dans cargo clippy
    Par exemple, la description de wip! revient à faire apparaître #![warn(clippy::todo)] en dehors de cargo clippy, au prix d’une nouvelle dépendance et d’une syntaxe non standard, et unwrap_wip ressemble aussi à #![warn(clippy::unwrap_used)]
    Pour moi, .unwrap() sert au prototypage, donc j’active #![warn(clippy::unwrap_used)], tandis que .expect("description of invariant") est destiné à la production, là où on pourrait utiliser .unwrap() au lieu de .unwrap_wip(), donc je n’active pas #![warn(clippy::expect_used)]. Exemple : .expect("parse regex from const")
    Je ne sais pas trop pour clone_wip et fixme!, et je n’y ai pas assez réfléchi pour répondre à la façon dont les développeurs étaient censés gérer ces cas

    • C’est peut-être honnête, mais ce n’est pas formulé de manière très charitable :-) J’utilise cet écosystème depuis 10 ans et, bien sûr, cela ne veut pas dire que je sais tout, mais on pourrait au moins en faire une lecture de bonne foi
      Cela dit, il est vrai que l’article ne traite pas des linters existants comme Clippy. C’est aussi lié à ma façon personnelle de travailler : auparavant, je lançais Clippy à chaque sauvegarde, mais dans notre projet c’était trop lent, avec plusieurs minutes entre deux sauvegardes, donc j’ai arrêté ; maintenant, il ne tourne que dans la CI et dans un hook avant push
      Ramener les avertissements dans rustc est bien plus rapide, et pouvoir voir des avertissements inline pendant les itérations devient central dans ce workflow
      La configuration de Clippy était aussi assez pénible, du moins la dernière fois que j’ai vérifié. Il n’y avait pas de moyen de définir d’un coup, dans le code, des lints pour toutes les crates d’un workspace : il fallait soit ajouter explicitement dans la CI un -D pour chaque lint que l’on voulait refuser dans le workspace, soit continuer à mettre des attributs globaux dans toutes les crates
      #![warn(clippy::todo)] doit être présent dans toutes les crates, donc pour nous ce n’est pas un gros avantage par rapport au fait d’ajouter/retirer une crate uniquement pendant le développement. Je pense que todo! devrait par défaut déclencher un avertissement dans rustc ou Clippy, et je suis d’accord que, si c’était le cas, l’utilité de wip diminuerait
      Je ne suis pas d’accord pour dire que unwrap_wip équivaut à #![warn(clippy::unwrap_used)], pour deux raisons. Premièrement, côté intention, la distinction selon laquelle unwrap serait hors production et expect serait pour la production est fréquente, mais pas universelle. Deuxièmement, notre base de code existante contient environ 4 600 unwrap, et nous pourrions tous les remplacer par expect, mais cela nous obligerait soit à insérer des messages temporaires partout, soit à passer beaucoup trop de temps à reformuler des préconditions du type « ce mutex n’a pas été empoisonné » ou « je veux propager cet échec d’opération à l’appelant »
      fixme! est très utile, et j’aimerais le voir entrer dans la bibliothèque standard ou ailleurs. C’est en pratique la fonctionnalité que nous utilisons le plus dans cette crate
      L’article n’entre pas dans ces détails, mais cette crate n’est pas un standard : c’est une bibliothèque spécialisée, avec aussi des fonctionnalités comme wip_iter. Elle permet d’utiliser des macros du type todo dans des fonctions qui retournent un impl Trait en position de retour pour des itérateurs, et wip_future fait la même chose. Il m’est souvent arrivé que l’inférence du type de retour et todo ne fonctionnent pas ensemble, m’obligeant à fabriquer un type de force. Quelqu’un a aussi proposé de produire par défaut une erreur de compilation en mode release, et nous allons examiner cela avec d’autres pistes d’amélioration
      Pour nuancer un peu le coût d’une nouvelle dépendance et d’une syntaxe non standard : c’est une dépendance d’un seul fichier, utilisée uniquement pendant le développement et généralement supprimée à la fin
  • Le problème quand on prototype avec unwrap, c’est que si l’on veut introduire correctement les erreurs plus tard, la forme du type de retour peut changer, surtout si ces fonctions sont utilisées dans un style fonctionnel, ce qui entraîne des effets en chaîne
    Je recommande de commencer avec un Result, en utilisant pour le type d’erreur Box<dyn Error>, une chaîne, anyhow, etc. On pourra ensuite passer à une vraie énumération d’erreurs, mais il est plus difficile de refactorer plus tard du code qui a l’air de ne pas pouvoir échouer pour lui faire retourner un Result
    Et si vous utilisez un LLM, soyez prudent. Un LLM est une machine à reconnaître des motifs : il ne distingue pas « j’ai volontairement fait ça vite fait et je veux le corriger plus tard » de « dans ce projet, les erreurs doivent être gérées ainsi ». Avec le temps, il peut faire proliférer des décisions prises « juste pour l’instant » ; quelques commentaires dans main indiquant où/quand une gestion laxiste des erreurs est acceptable peuvent aider

  • Mon interprétation de todo!() et unimplemented!() ne correspond pas à celle de la documentation. Cela dit, le paragraphe suivant, qui a été omis, rééquilibre en partie cette position
    La documentation dit que todo! communique l’intention d’implémenter plus tard, avec le message « not yet implemented », tandis que unimplemented! ne fait pas cette promesse et affiche « not implemented »
    Quand todo! a été ajouté en 1.40.0, c’était simplement un nom plus court, sans autre différence avec unimplemented!
    Ce n’est qu’en 1.42.0 que le « yet » a été retiré du message de unimplemented. Si je me souviens bien, fixer ainsi la signification des deux macros a été un peu controversé, tout comme le fait d’avoir livré todo! comme raccourci de unimplemented!
    Aujourd’hui, unimplemented!() est recommandé dans des cas comme des implémentations de traits où il faut fournir techniquement un corps de méthode, mais où celui-ci ne doit jamais être appelé. L’exemple de la documentation est précisément de cette forme

  • Je me demande si le flux consistant à ajouter la dépendance wip à une crate du workspace pendant le développement, puis à la retirer en réécrivant l’historique avant la review, pourrait être rendu plus fluide avec des outils supplémentaires
    Devoir aussi réécrire le fichier de lock en modifiant le milieu d’une PR me rebute un peu. Si l’arbre des dépendances a changé ailleurs entre-temps, cela peut créer des conflits inutiles. Peut-être qu’on pourrait abuser de jj fix. En tout cas, l’idée du projet est très bonne, et je pense que je finirai par l’utiliser quelque part

  • Une macro de prise de note qui ne panique pas, comme wip::fixme!, est vraiment une excellente idée. Le truc du commentaire de documentation est aussi une méthode élégante à garder en tête

  • Il y a des idées intéressantes qui pourraient idéalement déboucher sur des changements côté rustc. todo!() devrait vraiment déclencher un avertissement par défaut
    Ce serait encore mieux si les développeurs pouvaient désactiver ce type d’avertissements quand ils sont en mode hack. Trop d’avertissements peuvent être écrasants, et on lance souvent cargo dans une console de 19 lignes en bas de l’IDE. Pendant le développement, les avertissements peuvent noyer les vraies erreurs et les avertissements plus importants. Rien qu’une discipline consistant à toujours trier les erreurs à la fin aiderait à atténuer ce problème
    Par ailleurs, les noms .unwrap_wip() et .clone_wip() sont plus longs que les noms d’origine, donc trop longs. Je recommanderais des noms comme .du() et .dc(), pour abréger « dev unwrap » et « dev clone ». Ce serait plus pratique pour programmer rapidement

  • Ne pourrait-on pas ajouter automatiquement .clone() et .unwrap() là où c’est nécessaire ? Par exemple avec un flag ou une fonctionnalité du type #[wip(autoclone,autounwrap)]. Ce n’est sans doute pas possible, mais on peut toujours rêver

  • Chaque fois que je démarre un nouveau projet ou prototype, je mets ces lignes dans main.rs :
    #![allow(dead_code)]
    #![allow(unused_variables)]
    #![allow(unreachable_code)]
    Cela réduit beaucoup le bruit dans les premières phases