La syntaxe la plus subtile de Rust
(zkrising.com)let et const en Rust
letsert à 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")};
- Sous la forme
constdé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
constpeut ê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 UPPERCASEetconst 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
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.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.
Waouh... c’est assez déroutant. Comment Rust compte-t-il gérer ça ?