1 Punkte von GN⁺ 5 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • 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: unwrap und clone erzeugen keine Warnungen, todo! ist auf indirekte Warnungen angewiesen, und der ///-Trick kann je nach Position zu Compilerfehlern oder unbeabsichtigter Dokumentation führen
  • Das Crate wip macht temporäre Implementierungen, temporäres unwrap/clone und Notizen für spätere Korrekturen über wip!, unwrap_wip, clone_wip und fixme! immer als Warnung sichtbar
  • Meilisearch wandelt in CI mit -D warning Warnungen in Fehler um, um das Einschleusen von WIP-Code zu verhindern. Während der Entwicklung hinzugefügte wip-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 rustc das Kompilieren wegen Rückgaben von Result, 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 later hinterlassen, 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 warning aus 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 unwrap aufschieben

    • Wenn man unwrap auf Result oder Option verwendet, 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
  • Ownership-Probleme mit clone aufschieben

    • 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!()
  • 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 Result und Option
    • 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

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 Mutation
  • unwrap und clone erzeugen überhaupt keine Warnungen, man muss sie also vor dem finalen PR erneut durchlesen und entfernen
  • unwrap kann 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 wip ist 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::*;
  • wip stü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 wie todo!, 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, ein std-Makro zu shadowen; dadurch gab es Probleme bei der Prelude-Nutzung
  • unwrap_wip und clone_wip

    • unwrap_wip und clone_wip sind als Extension Traits für Result und Option implementiert
    • Wenn sie im Scope sind, verhalten sie sich wie normales unwrap bzw. clone, erzeugen bei Verwendung aber eine Warnung
    • So lässt sich klar signalisieren, dass nicht beabsichtigt ist, unwrap oder clone im finalen Code zu belassen
  • wip::fixme!

    • Das Makro fixme! ist eine nicht panickende Variante von wip! 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

Nutzungsablauf und Hinweise

  • Das Crate wip wurde 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
  • wip hilft 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 fixme nicht genügend Kontext notiert, ist eine spätere Korrektur schwierig
    • In großen PRs kann die Zahl der von wip erzeugten Warnungen groß und damit belastend werden
  • Die aktuelle API ist in der wip-Dokumentation auf docs.rs zu finden

1 Kommentare

 
GN⁺ 5 시간 전
Kommentare auf Lobste.rs
  • Ehrlich gesagt entsteht bei der Begründung für wip der 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; wip scheint bestehende Funktionen unter neuem Namen nachzubauen und Lints, die in cargo clippy lagen, mit Gewalt in Richtung cargo check und cargo build zu verschieben.
    Zum Beispiel sorgt die Beschreibung von wip! dafür, dass #![warn(clippy::todo)] auch außerhalb von cargo clippy auftaucht – zum Preis einer neuen Abhängigkeit und nicht standardmäßiger Syntax; unwrap_wip ist ä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_wip und fixme! 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.

    • Das mag ehrlich gemeint sein, ist aber nicht gerade freundlich formuliert :-) Ich nutze diesen Bereich seit zehn Jahren, und natürlich heißt das nicht, dass ich alles weiß, aber eine wohlwollende Interpretation wäre doch möglich.
      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 rustc holt, 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 -D in 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 von rustc oder Clippy gewarnt werden, und ich stimme zu, dass das den Nutzen von wip verringern würde.
      Dass unwrap_wip dasselbe sei wie #![warn(clippy::unwrap_used)], sehe ich aus zwei Gründen anders. Erstens ist die Unterscheidung „unwrap für Nicht-Produktionscode, expect für Produktionscode“ zwar verbreitet, aber nicht universell. Zweitens haben wir in der bestehenden Codebasis etwa 4600 unwraps; man könnte sie zwar alle in expect umwandeln, 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_iter gibt. Damit kann man in Funktionen mit impl Trait an der Rückgabeposition für Iteratoren Makros wie todo verwenden; wip_future funktioniert genauso. Ich hatte schon ziemlich oft Fälle, in denen Rückgabetyp-Inferenz und todo nicht 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 unwrap ist, 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 Result anzufangen und als Fehlertyp etwa Box<dyn Error>, Strings, anyhow usw. 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 auf Result-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!() und unimplemented!() 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ährend unimplemented! 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 von unimplemented!.
    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 von unimplemented! 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 fix dafü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 rustc führen. Für todo!() 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 cargo in 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.rs folgende Zeilen ein:
    #![allow(dead_code)]
    #![allow(unused_variables)]
    #![allow(unreachable_code)]
    Das reduziert in der Anfangsphase viel Rauschen.