WIP-Code in Rust als Warnung markieren
(blog.dureuill.net)- Wenn man während der Rust-Entwicklung alle Fehlerpfade und Ownership-Probleme sofort bereinigt, wird der Implementierungsfluss leicht unterbrochen. Eine nützliche Strategie ist daher, zuerst den happy path zu bauen und die verbleibenden Aufgaben als Warnungen nachzuverfolgen
- Schon mit Standard-Rust lassen sich
unwrap,clone,todo!, die Standardwarnungen des Compilers und ein Trick mit///-Dokumentationskommentaren kombinieren, um unfertigen Code zu markieren - Die bestehenden Methoden haben Grenzen:
unwrapundcloneerzeugen keine Warnungen,todo!ist auf indirekte Warnungen angewiesen, und der///-Trick kann je nach Position zu Compilerfehlern oder unbeabsichtigter Dokumentation führen - Das Crate
wipmacht temporäre Implementierungen, temporäresunwrap/cloneund Notizen für spätere Korrekturen überwip!,unwrap_wip,clone_wipundfixme!immer als Warnung sichtbar - Meilisearch wandelt in CI mit
-D warningWarnungen in Fehler um, um das Einschleusen von WIP-Code zu verhindern. Während der Entwicklung hinzugefügtewip-Abhängigkeiten werden im Zuge der History-Bereinigung vor dem Review wieder entfernt
Warum Warnungen während der Rust-Entwicklung nötig sind
- Rust ist eine Sprache, die Korrektheit in den Mittelpunkt stellt. Wenn man während der Entwicklung jedoch jeden Fehlerpfad sofort behandelt, kann das Implementierungstempo und die Konzentration leiden
- Wenn
rustcdas Kompilieren wegen Rückgaben vonResult, Fehlerbehandlung oder der Implementierung von Edge Cases verhindert, gibt es meist zwei Optionen- Sofort eine vollständige Fehlerbehandlung implementieren und sie bis zur PR-Reife mehrfach refaktorieren
- Eine Markierung im Sinne von
TODO laterhinterlassen, das Problem auf Warnungs-Niveau herabstufen und an der aktuellen Aufgabe weiterarbeiten
- Warnungen blockieren die Entwicklung nicht, bleiben aber als Signal erhalten, das vor dem Mergen eines PR bereinigt werden muss
- Meilisearch führt den Compiler in CI mit dem Flag
-D warningaus und wandelt so alle Warnungen in Fehler um, damit WIP-Code nicht versehentlich ausgeliefert wird - Code-Reviews sind eine grundlegende Praxis, um Bugs zu verhindern und Wissen im Team zu teilen. Wenn man sich aber nur auf menschliches Gedächtnis und Aufmerksamkeit verlässt, passieren irgendwann Fehler; deshalb sind zusätzliche Leitplanken nötig
Unfertige Arbeit mit Standard-Rust aufschieben
-
Fehlerbehandlung mit
unwrapaufschieben- Wenn man
unwrapaufResultoderOptionverwendet, kann man den inneren Wert entnehmen und mit der aktuellen Implementierung fortfahren, um den Preis eines Panic im Fehlerpfad - Sobald der restliche Code fertig ist, kann man die temporär hinzugefügten
unwrap-Aufrufe gezielt entfernen und durch passende Fehlerbehandlung ersetzen - Dieser Ansatz basiert auf der Annahme, dass Rust zwar die Struktur für korrekte Fehlerbehandlung bereitstellt, diese Behandlung aber nicht sofort abgeschlossen werden muss
- Wenn man
-
Ownership-Probleme mit
cloneaufschieben- Ownership-Probleme sind seltener als Fehlerbehandlung, doch korrekte Ownership kann Designänderungen erfordern
- In diesem Fall kann man einen Wert
clonen, statt ihn zu verschieben, und so Lifetime-Probleme vorübergehend umgehen - Später lässt sich erfahrungsgemäß erkennen, wann ein Clone entfernt werden kann, indem die passende Variable weiter oben im Stack übergeben wird
- Allerdings kann eine Korrektur Änderungen an der Codestruktur nach sich ziehen, weshalb das Aufschieben schwieriger ist als bei der Fehlerbehandlung
-
Mit
todo!()nur einen Implementierungsplatzhalter schaffen- Das Makro
todo!()aus der Rust-Standardbibliothek steht für unfertigen Code und ist beim Prototyping als Platzhalter nützlich, um die Typanalyse zu bestehen - Da
todo!()durch ein Panic divergiert, kann es an den meisten Typ-Positionen als Platzhalterausdruck verwendet werden - So kann man Funktionsdefinitionen bereits nutzen, während man Interfaces festlegt, und die eigentliche Implementierung auf später verschieben
todo!()ist weniger dazu gedacht, Programmnutzern eine nicht implementierte Funktion zu melden, sondern eher eine WIP-Komfortfunktion für Code-Autoren und Reviewer- Für den Zweck, Nutzern einen nicht implementierten Zustand zu melden, gibt es separat das Makro
unimplemented!()
- Das Makro
-
Standardwarnungen des Compilers und der Dokumentationskommentar-Trick
- Auch die Standardwarnungen des Rust-Compilers sind nützlich, um während der Entwicklung unfertige Zustände sichtbar zu machen
- Unbenutzte Bindings
- Überflüssiges
mut - Unbenutzte
ResultundOption
- Wenn man solche Warnungen während der Entwicklung mit einem
_-Präfix oder anderen Tricks vorübergehend entfernt, besteht die Gefahr, dass unfertiger Code einfach durchrutscht - Edge Cases oder fehlerhafte Logik, die nicht vom Typsystem nachverfolgt werden, werden manchmal als Kommentar hinterlassen
- Wenn man einen Kommentar so setzt, dass er wie ein
///-Dokumentationskommentar aussieht, warnt Rust bei Dokumentationskommentaren ohne Dokumentationsziel. So lässt sich ein Problem an dieser Stelle als Warnung nachverfolgen
- Auch die Standardwarnungen des Rust-Compilers sind nützlich, um während der Entwicklung unfertige Zustände sichtbar zu machen
Grenzen der Standardmethoden
- Das größte Problem ist, dass nicht alle Techniken zuverlässig eine Warnung erzeugen, die mit dem zu lösenden Problem verknüpft ist
todo!()löst häufig Warnungen aus, diese stammen aber oft aus indirekten Ursachen, etwa unbenutzten Funktionsargumenten oder nicht stattfindender notwendiger Mutationunwrapundcloneerzeugen überhaupt keine Warnungen, man muss sie also vor dem finalen PR erneut durchlesen und entfernenunwrapkann auch berechtigt in Produktionscode verbleiben, wodurch temporäre WIP-Nutzung und normale Nutzung vermischt werden können- Der Trick mit
///-Dokumentationskommentaren ist eher ein Hack; an Ausdruckspositionen sind Dokumentationskommentare verboten und können Compilerfehler verursachen - Wenn man
///an einer Stelle verwendet, an der tatsächlich Dokumentation erzeugt wird, erscheint die erwartete Warnung nicht, und im ausgelieferten Code kann unerwünschte Dokumentation zurückbleiben
Werkzeuge des Crates wip
- Das Crate
wipist eine Sammlung von Werkzeugen, die die Strategie „Warnungen für später zu erledigende Dinge“ während der Rust-Entwicklung komfortabler machen soll - Es lässt sich wie ein normales Crate hinzufügen
cargo add wip
- In benötigten Dateien kann man das Prelude importieren
use wip::prelude::*;
wipstützt sich auf Rusts Deprecation Warnings. Daher kann es weniger störend sein, die Editor-Funktion zu deaktivieren, die veraltete Funktionsaufrufe durchstreicht-
wip::wip!- Das Makro
wip!verhält sich wietodo!, erzeugt bei Verwendung aber immer eine Warnung - Es kann an Stellen genutzt werden, an denen man bisher
todo!verwendet hat, um unfertigen Code zu markieren - In einer frühen Version hieß es
todo, doch Rust bevorzugt es nicht, wenn eine Abhängigkeit versucht, einstd-Makro zu shadowen; dadurch gab es Probleme bei der Prelude-Nutzung
- Das Makro
-
unwrap_wipundclone_wipunwrap_wipundclone_wipsind als Extension Traits fürResultundOptionimplementiert- Wenn sie im Scope sind, verhalten sie sich wie normales
unwrapbzw.clone, erzeugen bei Verwendung aber eine Warnung - So lässt sich klar signalisieren, dass nicht beabsichtigt ist,
unwrapodercloneim finalen Code zu belassen
-
wip::fixme!- Das Makro
fixme!ist eine nicht panickende Variante vonwip!und ersetzt keinen Ausdruck - Es kann wie ein TODO-Kommentar genutzt werden, erzeugt aber ohne den
///-Trick zuverlässig eine Warnung - Es wird häufig verwendet, um Notizen zu hinterlassen, die später korrigiert werden müssen
- Das Makro
Nutzungsablauf und Hinweise
- Das Crate
wipwurde in einigen jüngeren PRs verwendet, darunter auch in einem größeren PR, Meilisearch PR #6484 - Der übliche Ablauf besteht darin, während der Entwicklung eine
wip-Abhängigkeit zu einem Crate im Workspace hinzuzufügen und sie beim Umschreiben der History vor dem Review wieder zu entfernen wiphilft dabei, sich auf den happy path zu konzentrieren und trotzdem später zu erledigende Aufgaben nicht zu vergessen- Es gibt auch Nachteile
- Wenn man beim Hinzufügen von
fixmenicht genügend Kontext notiert, ist eine spätere Korrektur schwierig - In großen PRs kann die Zahl der von
wiperzeugten Warnungen groß und damit belastend werden
- Wenn man beim Hinzufügen von
- Die aktuelle API ist in der
wip-Dokumentation auf docs.rs zu finden
1 Kommentare
Kommentare auf Lobste.rs
Ehrlich gesagt entsteht bei der Begründung für
wipder Eindruck, dass hier Clippy neu erfunden wurde, weil nicht verstanden wurde, wie die Tools eigentlich gedacht sind.Die Toolchain-Entwickler haben bisher unterschieden, was eine
rustc-Warnung ist und was ein Clippy-Lint wird;wipscheint bestehende Funktionen unter neuem Namen nachzubauen und Lints, die incargo clippylagen, mit Gewalt in Richtungcargo checkundcargo buildzu verschieben.Zum Beispiel sorgt die Beschreibung von
wip!dafür, dass#![warn(clippy::todo)]auch außerhalb voncargo clippyauftaucht – zum Preis einer neuen Abhängigkeit und nicht standardmäßiger Syntax;unwrap_wipist ähnlich wie#![warn(clippy::unwrap_used)].Für mich ist
.unwrap()fürs Prototyping, daher aktiviere ich#![warn(clippy::unwrap_used)];.expect("description of invariant")ist dagegen für Produktionscode, in dem man statt.unwrap_wip()eher.unwrap()verwenden würde, daher aktiviere ich#![warn(clippy::expect_used)]nicht. Beispiel:.expect("parse regex from const")Bei
clone_wipundfixme!bin ich mir nicht sicher, und ich habe nicht genug darüber nachgedacht, um beantworten zu können, wie die Entwickler solche Fälle gedacht haben.Allerdings stimmt es, dass im Artikel die Auseinandersetzung mit bestehenden Lintern wie Clippy fehlt. Das hat auch mit meiner persönlichen Nutzung zu tun: Früher habe ich Clippy beim Speichern laufen lassen, aber in unserem Projekt war das zu langsam, sodass zwischen Speicherungen mehrere Minuten lagen; deshalb habe ich es aufgegeben und lasse es heute nur noch in CI und in einem Pre-Push-Hook laufen.
Wenn man die Warnungen zu
rustcholt, ist das deutlich schneller, und man sieht Inline-Warnungen während der Iteration – das ist der Kern dieses Workflows.Auch die Clippy-Konfiguration war, zumindest beim letzten Mal, als ich nachgesehen habe, ziemlich umständlich. Es gab keine Möglichkeit, Lints für alle Crates eines Workspace auf einmal im Code festzulegen; man musste also für jeden Lint, den man im Workspace verbieten wollte, ein explizites
-Din CI ergänzen oder in allen Crates ständig globale Attribute einfügen.#![warn(clippy::todo)]wird in allen Crates gebraucht, daher ist das für uns kein großer Vorteil gegenüber dem Hinzufügen/Entfernen eines Crates nur während der Entwicklung. Ich denke,todo!sollte standardmäßig vonrustcoder Clippy gewarnt werden, und ich stimme zu, dass das den Nutzen vonwipverringern würde.Dass
unwrap_wipdasselbe sei wie#![warn(clippy::unwrap_used)], sehe ich aus zwei Gründen anders. Erstens ist die Unterscheidung „unwrapfür Nicht-Produktionscode,expectfür Produktionscode“ zwar verbreitet, aber nicht universell. Zweitens haben wir in der bestehenden Codebasis etwa 4600 unwraps; man könnte sie zwar alle inexpectumwandeln, aber dann würde man viel zu viel Zeit damit verbringen, überall provisorische Nachrichten einzufügen oder Vorbedingungen wie „dieser Mutex wurde nicht poisoned“ oder „ich möchte diesen Fehler an den Aufrufer weiterreichen“ neu zu formulieren.fixme!ist sehr nützlich, und ich fände es gut, wenn so etwas in die Standardbibliothek oder Ähnliches käme. Tatsächlich ist das die Funktion, die ich aus diesem Crate am häufigsten verwende.Der Artikel behandelt solche Details nicht, aber dieses Crate ist kein Standard, sondern eine spezialisierte Bibliothek, weshalb es auch Funktionen wie
wip_itergibt. Damit kann man in Funktionen mitimpl Traitan der Rückgabeposition für Iteratoren Makros wietodoverwenden;wip_futurefunktioniert genauso. Ich hatte schon ziemlich oft Fälle, in denen Rückgabetyp-Inferenz undtodonicht zusammen funktionierten und ich gezwungen war, einen Typ künstlich zu erzeugen. Jemand hat auch vorgeschlagen, im Release-Modus standardmäßig einen Kompilierfehler auszulösen; das werde ich mir zusammen mit anderen Verbesserungsideen ansehen.Um die Kosten durch neue Abhängigkeit und nicht standardmäßige Syntax etwas ausgewogener zu betrachten: Es handelt sich um eine Single-File-Abhängigkeit, die nur während der Entwicklung genutzt und am Ende normalerweise wieder entfernt wird.
Das Problem beim Prototyping mit
unwrapist, dass sich später, wenn man Fehler ordentlich einführt, die Form des Rückgabetyps ändern kann – besonders wenn diese Funktionen in funktionalem Stil verwendet werden, kann das Kettenreaktionen auslösen.Ich empfehle, mit
Resultanzufangen und als Fehlertyp etwaBox<dyn Error>, Strings,anyhowusw. zu verwenden. Später auf eine echte Fehler-Enum umzusteigen ist möglich; schwieriger ist es, Code, der so aussieht, als könne er nicht fehlschlagen, später aufResult-Rückgaben zu refaktorisieren.Außerdem sollte man vorsichtig sein, wenn man LLMs nutzt. LLMs sind Pattern-Matching-Maschinen und können nicht unterscheiden zwischen „das ist absichtlich grob gemacht und soll später repariert werden“ und „so soll dieses Projekt Fehler behandeln“. Mit der Zeit können sie Entscheidungen vom Typ „nur erst mal jetzt“ vermehren; ein oder zwei Kommentare in
main, die erklären, wo/wann lockere Fehlerbehandlung in Ordnung ist, können helfen.Bei der Interpretation von
todo!()undunimplemented!()sind die Dokumentation und ich nicht einer Meinung. Der ausgelassene folgende Absatz gleicht diese Position allerdings ein Stück weit aus.In der Dokumentation heißt es,
todo!vermittle die Absicht, es später zu implementieren, und die Meldung laute „not yet implemented“, währendunimplemented!kein solches Versprechen mache und die Meldung „not implemented“ sei.Als
todo!in 1.40.0 hinzugefügt wurde, war es rein ein kürzerer Name und unterschied sich außer durch den Namen nicht vonunimplemented!.Erst in 1.42.0 verschwand das „yet“ aus der Meldung von
unimplemented. Soweit ich mich erinnere, war es auch etwas umstritten, den beiden Makros diese Bedeutung zu geben; ebenso,todo!als Kurzform vonunimplemented!auszuliefern.Heute wird
unimplemented!()für Fälle empfohlen, in denen aus technischen Gründen ein Methodenrumpf bereitgestellt werden muss, dieser aber niemals aufgerufen werden darf, etwa bei Trait-Implementierungen. Genau so sieht auch das Beispiel in der Dokumentation aus.Ich frage mich, ob man den Ablauf, während der Entwicklung einem Crate im Workspace die
wip-Abhängigkeit hinzuzufügen und sie dann vor dem Review durch Umschreiben der Historie wieder zu entfernen, mit zusätzlichem Tooling glatter machen könnte.Während eines PRs auch noch die Lockfile neu schreiben zu müssen, fühlt sich etwas unangenehm an. Wenn sich in der Zwischenzeit der Dependency Tree an anderer Stelle geändert hat, kann das unnötige Konflikte verursachen. Vielleicht könnte man
jj fixdafür missbrauchen. Jedenfalls ist die Projektidee sehr gut, und ich kann mir vorstellen, sie irgendwo zu nutzen.Ein Notiz-Makro, das nicht panict, wie
wip::fixme!, ist wirklich eine gute Idee. Auch der Doc-Comment-Trick ist eine saubere Methode, die man sich merken sollte.Es gibt einige interessante Ideen, die hoffentlich zu Änderungen in
rustcführen. Fürtodo!()sollte es wirklich eine Standardwarnung geben.Noch besser wäre es, wenn Entwickler solche Warnungen abschalten könnten, wenn sie im Hack-Modus sind. Zu viele Warnungen sind überwältigend, und normalerweise lässt man
cargoin einer 19-Zeilen-Konsole unten in der IDE laufen. Während der Entwicklung begraben Warnungen echte Fehler und wichtigere Warnungen. Schon die Disziplin, Fehler immer ans Ende zu sortieren, könnte dieses Problem entschärfen.Außerdem sind die Namen
.unwrap_wip()und.clone_wip()länger als die ursprünglichen Namen und damit zu lang. Als Abkürzungen für „dev unwrap“ und „dev clone“ würde ich Namen wie.du()und.dc()vorschlagen. Beim schnellen Programmieren wären sie leichter zu benutzen.Könnte man
.clone()und.unwrap()nicht dort, wo sie nötig sind, automatisch hinzufügen lassen? Zum Beispiel über Flags oder Features wie#[wip(autoclone,autounwrap)]. Wahrscheinlich geht das nicht, aber träumen darf man.Jedes Mal, wenn ich ein neues Projekt oder einen Prototyp starte, füge ich in
main.rsfolgende Zeilen ein:#![allow(dead_code)]#![allow(unused_variables)]#![allow(unreachable_code)]Das reduziert in der Anfangsphase viel Rauschen.