La sintaxis más sutil de Rust
(zkrising.com)let y const en Rust
letse usa para declarar una variable nueva- Tiene la forma
let PAT = EXPR;y es más potente de lo que parece - Combinado con pattern matching, ofrece funciones muy convenientes
let (a, b) = (5, 10);let maybe_string: Option<String> = ..;let Some(value) = maybe_string else { panic!("die horribly")};
- Tiene la forma
constes una constante que se calcula en tiempo de compilación y se incrusta directamente en el código compiladoconst MY_VAR: &str = "heyyyyyyyy man";const SECRET: i32 = 0x1234;- Tiene la forma
const IDENT: TYPE = EXPR;, requiere especificar el tipo y no puede usar patrones
Lo que confunde
constse puede usar sin importar el orden de declaración (hoisting)
// Compila aunque X esté definido después de Y
const Y: i32 = X + X;
const X: i32 = 5;
- También se puede declarar dentro de funciones, y aun así puede aplicarse hoisting
fn oh_boy() -> i32 {
return X;
const X: i32 = 5;
// ^ Compila y funciona. ¡Sin warning!
}
- Si trabajas con programadores que vienen de JavaScript y apenas están aprendiendo Rust, esta es una gran función para dejarlos desconcertados
- Es una consecuencia inocente de una gran función, pero ahora vamos a escribir consecuencias dañinas
El match de Rust
// let PAT = EXPR;
let x = 5;
// Aquí, `x` es un patrón. Comprueba si puede meter `5` en `x`
// Este patrón siempre hace match: siempre puedes meter 5 en una variable llamada `x`
// No todos los patrones tienen que hacer match necesariamente. Por ejemplo:
let (5, x) = (a, b);
// Aquí la expresión solo hace "match" con el patrón si a == 5
//
// A esto se le llama un patrón "refutable"
//
// En una declaración `let`, un patrón refutable tiene que manejar el caso "rechazado":
let (5, x) = (a, b) else { panic!() };
//
// ...de lo contrario podrías terminar con una variable que "existe condicionalmente", y eso no está bien
- Entonces, veamos
match. ¿Qué esmatch?
// `match` es una lista de patrones y la acción a realizar si hacen match
//
// match EXPR {
// PAT => EXPR
// PAT => EXPR
// ..
// }
match (a, b) {
(5, x) => {
// Si (a,b) hace match con (5,x), se ejecuta este bloque
},
(x, 5) => {
// De la misma forma: si (a,b) hace match con (x, 5)...
},
(x, y) => {
// Y este es un patrón "captura-todo", igual que funciona let (x,y) = (a,b)
}
}
Vamos a causar dolor
- Confundir a la gente ya es divertido, pero ¿qué tal causar desgracia total y bugs reales?
- En mi opinión, esta es la sintaxis más sutil de Rust:
- La línea más interesante de este artículo: la sintaxis más sutil de Rust es que las constantes en sí mismas son patrones
- Esta sintaxis agrega algunas ergonomías útiles alrededor del matching:
let input: i32 = ..;
const GOOD: i32 = 1;
const BAD: i32 = 2;
match input {
// Esto comprueba si input == GOOD, porque GOOD es una constante
GOOD => println!("input was 1"),
// Esto comprueba si input == BAD, porque BAD es una constante.
BAD => println!("input was 2"),
// Esto define otherwise = input y siempre hace match...
otherwise => println!("input was {otherwise}"),
}
Pero escribir constantes en mayúsculas es solo una convención. El compilador apenas te da un warning por no hacerlo.
const good: i32 = 1;
const bad: i32 = 2;
match input {
// Eh...
good => {},
bad => {},
otherwise => {},
}
Ahora tenemos tres ramas que se ven iguales, pero lo que hacen depende de si existe o no una constante con ese nombre.
Vamos a empeorarlo. ¿Qué pasa aquí abajo?
const GOOD: i32 = 1;
match input {
// Typo...
GOD => println!("input was 1"),
otherwise => println!("input was not 1")
}
Aquí el compilador mostrará un warning, pero este código siempre imprimirá input was 1
O, de forma un poco más realista:
// Ups, comentaste o borraste este import por accidente
// use crate::{SOME_GL_CONSTANT, OTHER_THING}
// ¡Ups!
match value {
SOME_GL_CONSTANT => ..,
OTHER_THING => ..,
_ => ..,
}
Esto confunde a la gente. Especialmente cuando intentan hacer cosas elegantes con enums.
enum MyEnum {
A, B, C
}
// Normalmente se escribe así
match value {
MyEnum::A => ..,
MyEnum::B => ..,
MyEnum::C => ..,
}
// Pero también puedes escribirlo así
use MyEnum::*;
match value {
A => {},
B => {},
C => {}
}
// Y luego, si cambias MyEnum...
enum MyEnum { A, B, D, E };
use MyEnum::*;
// ¡Esto sigue compilando!
match value {
A => {},
B => {},
C => {},
}
// `C` ahora se convierte en un patrón "captura-todo", porque ya no existe nada como `C` en el scope.
// Estás haciendo let C = value, y eso siempre hace match!!!
Clippy tiene muchas reglas que te advierten que no hagas esto, porque esto siempre confunde a la gente.
Pero se puede volver todavía más confuso:
// Vincula x a 5 de manera irrefutable...
let x = 5;
// ...espera un momento...
const x: i32 = 4;
Este código no compila. Porque const x es un patrón, la constante se eleva por hoisting, y ahora este código se evalúa así:
let 4 = 5;
// error[E0005]: refutable pattern in local binding
// --> src/main.rs:3:5
// |
// 3 | let x = 5;
// | ^
// | |
// | los patrones `i32::MIN..=3_i32` y `5_i32..=i32::MAX` no están cubiertos
// | faltan patrones porque `x` se interpreta como un patrón constante, no como una variable nueva
// | ayuda: introduce una variable en su lugar: `x_var`
// |
// = nota: los bindings `let` requieren un "patrón irrefutable", como una `struct` o un `enum` con una sola variante
"expr es igual a 4" no es un match irrefutable, y no maneja el caso en que no lo sea
Cómo fastidiar a todos a tu alrededor
// Supón que `maybe` es un Option<&str>. Podría ser algún texto, o podría ser None.
let maybe_username: Option<&str> = ..;
// Este es el patrón común de Rust en un match de una sola línea. Si hace match con Some(..), podemos hacer algo con ese string.
if let Some(username) = maybe_username {
// Así que este código se ejecuta si username existe...
return username.to_uppercase();
}
// Pero... ahora ese código solo se ejecuta si 'username' hace match con Some("hey")
const username: &str = "hey";
La combinación de hoisting de constantes y el hecho de que las constantes sean patrones te permite escribir código Rust verdaderamente enigmático
Esto no es un problema real
- En la práctica, la única razón por la que esto puede ser confuso es que puedes escribir
let UPPERCASEyconst lowercase - Si crear variables que empiecen con mayúscula fuera un error de lint, la confusión no ocurriría
- Porque no podrías vincular algo accidentalmente cuando intentabas hacer match con una variante de enum o una constante
- Pero para ser claros, esto es solo una rareza divertida del lenguaje
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 comentarios
En realidad no es un gran problema, porque en la mayoría de los entornos de desarrollo hay un servidor de lenguaje
y ahí infiere todo y te lo muestra.
rust-analyzer, que es la base del servidor de lenguaje de RustRover, es una herramienta bastante potente.Simplemente juntan los patrones oscuros que existen en cualquier lenguaje
esto puede provocar confusión.
Es un artículo con esa clase de tono, ¿no?
Uf... está de locos. ¿Cómo piensa Rust manejar esto?