14 points par xguru 2024-11-05 | 3 commentaires | Partager sur WhatsApp

let et const en Rust

  • let sert à déclarer une nouvelle variable
    • Sous la forme let PAT = EXPR;, c’est plus puissant qu’il n’y paraît
    • Combiné au pattern matching, il offre des fonctionnalités pratiques
      • let (a, b) = (5, 10);
      • let maybe_string: Option<String> = ..;
      • let Some(value) = maybe_string else { panic!("die horribly")};
  • const désigne une constante calculée à la compilation et directement intégrée dans le code compilé
    • const MY_VAR: &str = "heyyyyyyyy man"; const SECRET: i32 = 0x1234;
    • Sous la forme const IDENT: TYPE = EXPR;, le type doit être explicite et on ne peut pas utiliser de pattern

Ce qui prête à confusion

  • const peut être utilisé quel que soit l’ordre de déclaration (hoisting)
// Compile même si X est défini après Y  
const Y: i32 = X + X;  
const X: i32 = 5;  
  • On peut aussi le déclarer à l’intérieur d’une fonction, et le hoisting fonctionne toujours
fn oh_boy() -> i32 {  
	return X;  
	const X: i32 = 5;  
	// ^ ça compile et ça fonctionne. Aucun warning !  
}  
  • Si vous travaillez avec un programmeur venant de JavaScript et commençant juste Rust, cette fonctionnalité peut parfaitement le dérouter
  • C’est une conséquence inoffensive d’une fonctionnalité intéressante, mais écrivons maintenant les conséquences nuisibles

Le match de Rust

// let PAT = EXPR;  
let x = 5;  
  
// Ici, `x` est un pattern. On vérifie si on peut mettre `5` dans `x`  
// Ce pattern matche toujours -- on peut toujours mettre 5 dans une variable nommée `x`  
  
// Tous les patterns ne matchent pas nécessairement. Par exemple :  
let (5, x) = (a, b);  
// Ici, l’expression ne "matche" le pattern que si a == 5  
//  
// On appelle cela un pattern "réfutable"  
//  
// Dans une déclaration `let`, il faut gérer le cas où un pattern réfutable est "refusé" :  
let (5, x) = (a, b) else { panic!() };  
//  
// ...sinon on pourrait se retrouver avec une variable "n’existant que conditionnellement", ce qui n’est pas souhaitable  
  • Voyons maintenant match. Qu’est-ce que c’est ?
// match est une liste de patterns et de ce qu’il faut faire s’ils correspondent  
//  
// match EXPR {  
//    PAT => EXPR  
//    PAT => EXPR  
//    ..  
// }  
  
match (a, b) {  
	(5, x) => {  
		// Si (a,b) matche (5,x), ce bloc s’exécute  
	},  
	(x, 5) => {  
		// De même : si (a,b) matche (x, 5)..  
	},  
	(x, y) => {  
		// Et ceci est un pattern "attrape-tout", identique à la façon dont fonctionne let (x,y) = (a,b)  
	}  
}  

Faisons souffrir un peu

  • Dérouter les gens est amusant, mais qu’en est-il de provoquer une vraie misère et de vrais bugs ?
  • À mes yeux, c’est la syntaxe la plus subtile de Rust :
    • La phrase la plus intéressante de cet article : la syntaxe la plus subtile de Rust est que les constantes sont elles-mêmes des patterns
  • Cette syntaxe ajoute quelques ergonomies utiles autour du matching :
let input: i32 = ..;  
  
const GOOD: i32 = 1;  
const BAD: i32 = 2;  
  
match input {  
	// Ceci vérifie si input == GOOD, parce que GOOD est une constante  
	GOOD => println!("input was 1"),  
	// Ceci vérifie si input == BAD, parce que BAD est une constante.  
	BAD => println!("input was 2"),  
	// Ceci définit otherwise = input, et matche donc toujours...  
	otherwise => println!("input was {otherwise}"),  
}  

Mais écrire les constantes en majuscules n’est qu’une convention. Au pire, le compilateur ne produit qu’un warning si on ne le fait pas.

const good: i32 = 1;  
const bad: i32 = 2;  
match input {  
	// Hum...  
	good => {},  
	bad => {},  
	otherwise => {},  
}  

Nous avons maintenant trois branches qui se ressemblent, mais leur comportement dépend de l’existence ou non de constantes portant ces noms !
Allons encore plus loin. Que se passe-t-il ci-dessous ?

const GOOD: i32 = 1;  
match input {  
	// Une faute de frappe...  
	GOD => println!("input was 1"),  
	otherwise => println!("input was not 1")  
}  

Ici, le compilateur affichera un warning, mais ce code imprimera toujours input was 1
Ou, de façon plus réaliste :

// Oups, cet import a été commenté ou supprimé par erreur  
// use crate::{SOME_GL_CONSTANT, OTHER_THING}  
  
// Aïe !  
match value {  
	SOME_GL_CONSTANT => ..,  
	OTHER_THING => ..,  
	_ => ..,  
}  

Cela perturbe les gens. Surtout quand ils essaient de faire des choses élégantes avec des enums.

enum MyEnum {  
	A, B, C  
}  
  
// Normalement, on écrit ceci  
match value {  
	MyEnum::A => ..,  
	MyEnum::B => ..,  
	MyEnum::C => ..,  
}  
  
// Mais on peut aussi écrire ceci  
use MyEnum::*;  
match value {  
	A => {},  
	B => {},  
	C => {}  
}  
// Et ensuite, si on modifie MyEnum...  
enum MyEnum { A, B, D, E };  
use MyEnum::*;  
  
// Ça compile toujours !  
match value {  
	A => {},  
	B => {},  
	C => {},  
}  
  
// `C` devient maintenant un pattern "attrape-tout", parce qu’il n’existe plus rien de ce nom dans la portée.  
// En réalité, vous faites let C = value, ce qui matche toujours !!!  

Clippy a de nombreuses règles qui avertissent de ne pas faire cela, précisément parce que cela perturbe tout le monde.
Mais on peut rendre la chose encore plus déroutante :

// Liaison irréfutable de x à 5...  
let x = 5;  
  
// ...attendez une seconde...  
const x: i32 = 4;  

Ce code ne compile pas, parce que const x est un pattern, que les constantes sont hoistées, et que ce code est maintenant évalué comme ceci :

let 4 = 5;  
  
// error[E0005]: refutable pattern in local binding  
//  --> src/main.rs:3:5  
//   |  
// 3 | let x = 5;  
//   |     ^  
//   |     |  
//   |     les patterns `i32::MIN..=3_i32` et `5_i32..=i32::MAX` ne sont pas couverts  
//   |     des patterns sont manquants parce que `x` est interprété comme un pattern constant, et non comme une nouvelle variable  
//   |     aide : introduisez plutôt une variable : `x_var`  
//   |  
//   = note : les liaisons `let` nécessitent un "irrefutable pattern", par exemple un `struct` ou un `enum` avec une seule variante  

"expr est égal à 4" n’est pas un match irréfutable, et ne gère pas le cas contraire

Agacer absolument tout le monde

// Supposons que `maybe` soit un Option<&str>. Il peut contenir du texte, ou bien None.  
let maybe_username: Option<&str> = ..;  
  
// C’est un pattern Rust très courant dans un match sur une seule ligne. Si cela matche Some(..), on peut faire quelque chose avec cette chaîne.  
if let Some(username) = maybe_username {  
	// Donc ce code s’exécute si username existe...  
	return username.to_uppercase();  
}  
  
// Sauf que... désormais, ce code ne s’exécute que si cela matche Some("hey")  
const username: &str = "hey";  

La combinaison du hoisting des constantes et du fait que les constantes sont des patterns permet d’écrire du code Rust assez ésotérique

Ce n’est pas vraiment un problème

  • En pratique, la seule raison pour laquelle cela peut être déroutant, c’est qu’on peut écrire let UPPERCASE et const lowercase
  • Si créer une variable commençant par une majuscule était une erreur de lint, la confusion n’existerait pas
    • On ne pourrait pas lier accidentellement une valeur alors qu’on voulait matcher une variante d’enum ou une constante
  • Mais soyons clairs : ce n’est qu’une bizarrerie amusante du langage
macro_rules! f {  
  ($cond: expr) => {  
    if let Some(x) = $cond {  
      println!("i am some == {x}!");  
    } else {  
      println!("i am none");  
    }  
  }  
}  
  
fn main() {  
    f!(Some(100));  
  
    {  
        f!(Some(100));  
        return;  
  
        const x: i32 = 5;  
    }  
}  

3 commentaires

 
sunrabbit 2024-11-05

En fait, ce n’est pas vraiment un gros problème, car la plupart des environnements de développement disposent d’un language server
qui infère tout et l’affiche.

rust-analyzer, qui sert de base au language server de RustRover, est un outil assez puissant.

 
sunrabbit 2024-11-05

C’est simplement un article qui rassemble les dark patterns qu’on peut trouver dans n’importe quel langage,
pour dire : ça, ça peut prêter à confusion !

C’est ce genre d’article, quoi.

 
kayws426 2024-11-05

Waouh... c’est assez déroutant. Comment Rust compte-t-il gérer ça ?