Laisser du code WIP sous forme d’avertissements en Rust
(blog.dureuill.net)- 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
unwrapniclone,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
wiprend les implémentations temporaires, lesunwrap/clonetemporaires et les notes à corriger plus tard toujours visibles sous forme d’avertissements viawip!,unwrap_wip,clone_wipetfixme! - Meilisearch transforme les avertissements en erreurs dans la CI avec
-D warningpour empêcher l’introduction de code WIP, et utilise un flux où la dépendancewipajouté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
rustcbloque la compilation pour des raisons comme le retour d’unResult, 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 warningdans 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
unwrapsur unResultou unOptionpermet 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
unwrapajouté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
- Utiliser
-
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
cloneau 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!()
- La macro
-
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
mutinutileResultetOptionnon 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
- Les avertissements par défaut du compilateur Rust sont également utiles pour faire apparaître des états inachevés pendant le développement
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 lieuunwrapetclonen’émettent aucun avertissement, il faut donc relire le code avant la PR finale pour les supprimerunwrappeut 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
wipest 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::*;
wips’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 commetodo!, 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 macrostd, Rust ne privilégiait pas ce comportement, ce qui posait problème avec l’utilisation du prelude
- La macro
-
unwrap_wipetclone_wipunwrap_wipetclone_wipsont implémentées comme des extension traits pourResultetOption- Lorsqu’elles sont dans le scope, elles se comportent comme
unwrapetclone, mais émettent un avertissement à l’utilisation - Elles permettent de signaler clairement que l’intention n’est pas de laisser un
unwrapou unclonedans le code final
-
wip::fixme!- La macro
fixme!est une variante non-panicking dewip!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
- La macro
Flux d’utilisation et points d’attention
- La crate
wipa é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 wipaide à 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
wippeut devenir pesant
- Si l’on n’écrit pas assez de contexte en ajoutant un
- L’API actuelle est consultable dans la documentation
wipsur docs.rs
1 commentaires
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ésLes développeurs de la toolchain distinguent ce qui relève d’un avertissement
rustcet ce qui relève d’un lint Clippy, maiswipsemble recréer des fonctionnalités existantes sous un nouveau nom et déplacer de force verscargo checketcargo builddes lints qui se trouvaient danscargo clippyPar exemple, la description de
wip!revient à faire apparaître#![warn(clippy::todo)]en dehors decargo clippy, au prix d’une nouvelle dépendance et d’une syntaxe non standard, etunwrap_wipressemble 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_wipetfixme!, 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 casCela 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
rustcest bien plus rapide, et pouvoir voir des avertissements inline pendant les itérations devient central dans ce workflowLa 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
-Dpour 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 quetodo!devrait par défaut déclencher un avertissement dansrustcou Clippy, et je suis d’accord que, si c’était le cas, l’utilité dewipdiminueraitJe 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 laquelleunwrapserait hors production etexpectserait 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 parexpect, 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 crateL’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 typetododans des fonctions qui retournent unimpl Traiten position de retour pour des itérateurs, etwip_futurefait la même chose. Il m’est souvent arrivé que l’inférence du type de retour ettodone 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éliorationPour 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îneJe recommande de commencer avec un
Result, en utilisant pour le type d’erreurBox<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 unResultEt 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
mainindiquant où/quand une gestion laxiste des erreurs est acceptable peuvent aiderMon interprétation de
todo!()etunimplemented!()ne correspond pas à celle de la documentation. Cela dit, le paragraphe suivant, qui a été omis, rééquilibre en partie cette positionLa documentation dit que
todo!communique l’intention d’implémenter plus tard, avec le message « not yet implemented », tandis queunimplemented!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 avecunimplemented!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 deunimplemented!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 formeJe 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émentairesDevoir 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 partUne 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êteIl 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éfautCe 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
cargodans 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èmePar 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 rapidementNe 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êverChaque 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