14 puntos por xguru 2024-11-05 | 3 comentarios | Compartir por WhatsApp

let y const en Rust

  • let se 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")};
  • const es una constante que se calcula en tiempo de compilación y se incrusta directamente en el código compilado
    • const 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

  • const se 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é es match?
// `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 UPPERCASE y const 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

 
sunrabbit 2024-11-05

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.

 
sunrabbit 2024-11-05

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?

 
kayws426 2024-11-05

Uf... está de locos. ¿Cómo piensa Rust manejar esto?