HeadlinesBriefing favicon HeadlinesBriefing.com

Parse Don't Validate in Rust: Nicht-leere Vektoren

Hacker News •
×

Wie viele Programmierer finde ich den Artikel von Alexis King über „Parse, don't validate“ faszinierend, weil er einem Idiom einen Namen gibt, das mir vertraut und wichtig erscheint – einem, das ich in der Vergangenheit beobachtet und verwendet habe, ohne es explizit zu benennen. Dieser Artikel ist eine Übersicht über das Muster „Parse, don't validate“, das auf die Programmiersprache Rust angewendet wird (der ursprüngliche Artikel verwendet Haskell). Ich war besonders daran interessiert, lehrreiche Beispiele für dieses Muster in der Rust-Standardbibliothek und in anderen bekannten Projekten zu finden. Ohne den Originalartikel zu wiederholen (lesen Sie ihn bitte zuerst!), hier ist sein Kern. Betrachten Sie den klassischen Vec; seine erste Methode gibt Option<&T> zurück. Warum? Weil ein Vektor nicht garantiert Elemente enthält, was passiert, wenn first auf einem leeren Vektor aufgerufen wird? In diesem Fall ist es in Rust idiomatisch [1], einen Option zurückzugeben, mit einem praktischen Syntax-Zucker, um das Ergebnis von Funktionen, die Option zurückgeben, zu akzeptieren und zu entscheiden, was als Nächstes zu tun ist. Also, wo liegt das Problem? Stellen wir uns vor, wir haben eine Funktion, die einige Konfigurations-Pfade aus einer Umgebungsvariablen liest und dabei die Invariante erzwingt, dass die Liste nicht leer sein darf: use anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories: Vec<Path Buf> = value.split(',').map(str::trim).map(Path Buf::from).collect(); ensure!(!directories.is_empty(), "empty CONFIG_DIRS"); Ok(directories) } Bisher alles gut. Betrachten wir nun einen typischen Anwendungsfall dieser Funktion: fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; match config_dirs.first() { Some(cache_dir) => initialize_cache(cache_dir), None => unreachable!("already checked that CONFIG_DIRS is non-empty"); } Ok(()) } Sobald get_configuration_directories einen erfolgreichen Wert zurückgibt, sind wir garantiert, dass der Vektor nicht leer ist. Und doch, wenn wir das erste Element dieses Vektors abrufen möchten, müssen wir die Methode first verwenden, die Option<&T> zurückgibt. Wir sind also erneut gezwungen, einen potenziell leeren Fall zu behandeln (wobei die Option None ist). Wie der Originalartikel feststellt, hat dies eine Reihe von Problemen in Bezug auf Code-Klarheit, potenzielle Leistungsauswirkungen und eine tickende Zeitbombe, falls die Invariante in get_configuration_directories jemals geändert wird. Das Kernproblem ist, dass Vec grundlegend ein Typ ist, der leer sein kann; wir können einen Kommentar „Dieser Vektor kann nicht leer sein, ich verspreche es!“ auf allen relevanten Code schreiben, aber es wird formal von nichts überprüft. Ein Typ für „nicht-leere“ Vektoren Die Lösung besteht darin, das Typsystem zu nutzen, um eine neu etablierte Invariante durchzusetzen. Wir können einen separaten Typ für „einen Vektor, der nicht leer sein kann“ verwenden; tatsächlich existieren solche Typen bereits in mehreren Rust-Crates – zum Beispiel nonempty: pub struct Non Empty<T> { pub head: T, pub tail: Vec<T>, } Dieser Typ hat keinen Konstruktor, der „keine Elemente“ zulässt; sein new nimmt ein Element, und seine first-Methode gibt &T ohne Option zurück: pub const fn new(e: T) -> Self { Self::singleton(e) } pub const fn singleton(head: T) -> Self { Non Empty { head, tail: Vec::new() } } pub const fn first(&self) -> &T { &self.head } Der Rest des Crates beschäftigt sich damit, Non Empty so nah wie möglich an ein normalen Vec anzupassen, indem viele nützliche Traits implementiert werden, sowie Konvertierungen wie: pub fn from_vec(mut vec: Vec<T>) -> Option<Non Empty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(Non Empty { head, tail: vec }) } } Sehen wir uns an, wie unsere get_configuration_directories-Funktion aussehen würde, wenn sie einen Non Empty anstelle eines normalen Vec zurückgibt: fn get_configuration_directories() -> Result<Non Empty<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(Path Buf::from).collect(); let Some(directories) = Non Empty::from_vec(directories) else { bail!("CONFIG_DIRS cannot be empty"); }; Ok(directories) } Beachten Sie hier die Verwendung von Non Empty::from_vec – dies ist der Ort, an dem die Invariante etabliert wird. Nun...

FAQ Frage: Was ist der Hauptvorteil der Verwendung eines Typs für nicht-leere Vektoren in Rust?

FAQ Antwort: Der Hauptvorteil besteht darin, dass die Invariante der Nicht-Leerheit auf Typ-Ebene durchgesetzt wird, was die Notwendigkeit von Laufzeit-Überprüfungen und der Behandlung von Option eliminiert, wenn auf das erste Element zugegriffen wird, was die Code-Klarheit und -Sicherheit verbessert.