HeadlinesBriefing favicon HeadlinesBriefing.com

Parse Don't Validate en Rust : vecteurs non vides

Hacker News •
×

Comme de nombreux programmeurs, je trouve l'article d'Alexis King sur « Parse, don't validate » fascinant, car il donne un nom à une pratique qui me semble familière et importante, une pratique que j'ai observée et utilisée dans le passé sans la nommer explicitement. Cet article est une revue du modèle « Parse, don't validate » appliqué au langage de programmation Rust (l'article original utilise Haskell). J'étais particulièrement intéressé par la recherche d'exemples éducatifs de ce modèle dans la bibliothèque standard de Rust et dans d'autres projets bien connus. Sans répéter l'article original (veuillez le lire d'abord !), voici son essence. Considérons le vénérable Vec ; sa première méthode retourne Option<&T>. Pourquoi ? Parce qu'un vecteur n'est pas garanti d'avoir des éléments, que faire si first est appelé sur un vecteur vide ? Retourner un Option dans ce cas est une pratique idiomatique en Rust [1], avec un sucre syntaxique pratique pour accepter le résultat de fonctions qui retournent Option et décider quoi faire ensuite. Alors quel est le problème ? Imaginons que nous ayons une fonction pour lire certains chemins de configuration à partir d'une variable d'environnement, tout en imposant l'invariant que la liste ne peut pas être vide : 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) } Jusqu'ici, tout va bien. Examinons maintenant un usage typique de cette fonction : 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(()) } Une fois que get_configuration_directories retourne un résultat réussi, nous sommes garantis que le vecteur n'est pas vide. Et pourtant, si nous voulons obtenir le premier élément de ce vecteur, nous devons utiliser la méthode first qui retourne Option<&T>. Nous sommes donc forcés - encore une fois - de gérer un cas potentiellement vide (où l'option est None). Comme le dit l'article original, cela pose plusieurs problèmes en termes de clarté du code, d'implications potentielles sur les performances et d'une bombe à retardement si l'invariant est jamais modifié dans get_configuration_directories. Le problème central est que Vec est fondamentalement un type qui peut être vide ; nous pouvons écrire un commentaire « Ce ne peut pas être vide, je promets ! » sur tout le code pertinent, mais cela n'est pas formellement vérifié par quoi que ce soit. Un type pour un vecteur « non vide » La solution est d'exploiter le système de types pour imposer un invariant nouvellement établi. Nous pouvons utiliser un type distinct pour un « vecteur qui ne peut pas être vide » ; en fait, de tels types existent déjà dans plusieurs crates de Rust - par exemple, nonempty: pub struct Non Empty<T> { pub head: T, pub tail: Vec<T>, } Ce type n'a pas de constructeur qui permette « aucun élément » ; son new prend un élément, et sa méthode first retourne &T sans Option : 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 } Le reste du crate s'occupe de faire en sorte que Non Empty se comporte aussi prochement que possible d'un Vec normal, en implémentant de nombreux traits utiles, ainsi que des conversions comme : 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 }) } } Voyons comment notre fonction get_configuration_directories ressemblerait si elle retournait un Non Empty au lieu d'un Vec simple : 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) } Notez l'utilisation de Non Empty::from_vec ici - c'est là que l'invariant est établi. Maintenant...

Question FAQ : Quel est le principal avantage d'utiliser un type de vecteur non vide en Rust ?

Réponse FAQ : Le principal avantage est qu'il impose l'invariant de non-vide au niveau du type, éliminant ainsi le besoin de vérifications en temps réel et de gestion des options lors de l'accès au premier élément, améliorant ainsi la clarté et la sécurité du code.