14 pontos por xguru 2024-11-05 | 3 comentários | Compartilhar no WhatsApp

let e const em Rust

  • let é usado para declarar novas variáveis
    • Na forma let PAT = EXPR;, e é mais poderoso do que parece
    • Combinado com pattern matching, oferece recursos convenientes
      • let (a, b) = (5, 10);
      • let maybe_string: Option<String> = ..;
      • let Some(value) = maybe_string else { panic!("die horribly")};
  • const são constantes avaliadas em tempo de compilação e incluídas diretamente no código compilado
    • const MY_VAR: &str = "heyyyyyyyy man"; const SECRET: i32 = 0x1234;
    • Na forma const IDENT: TYPE = EXPR;, é obrigatório especificar o tipo e não se pode usar pattern

O que causa confusão

  • const pode ser usado independentemente da ordem da declaração (hoisting)
// Compila mesmo que X seja definido depois de Y  
const Y: i32 = X + X;  
const X: i32 = 5;  
  • Também pode ser declarado dentro de funções, e mesmo assim continua havendo hoisting
fn oh_boy() -> i32 {  
	return X;  
	const X: i32 = 5;  
	// ^ compila e funciona. Sem warning!  
}  
  • Se você trabalha com alguém vindo de JavaScript e começando em Rust agora, esse recurso é ótimo para deixá-lo desnorteado
  • Isso é uma consequência inofensiva de um ótimo recurso, então agora vamos escrever uma consequência nociva

Match em Rust

// let PAT = EXPR;  
let x = 5;  
  
// Aqui, `x` é um pattern. Verifica se `5` pode ser colocado em `x`  
// Esse pattern sempre casa -- sempre é possível colocar 5 na variável chamada `x`  
  
// Nem todo pattern precisa casar obrigatoriamente. Por exemplo:  
let (5, x) = (a, b);  
// Aqui a expressão só "casa" com o pattern se a == 5  
//  
// Isso é chamado de pattern "refutável"  
//  
// Em declarações `let`, patterns refutáveis precisam lidar com o caso de "recusa":  
let (5, x) = (a, b) else { panic!() };  
//  
// ...caso contrário, você poderia acabar com uma variável que "existe condicionalmente", o que não é bom  
  • Então vamos falar de match. O que é um match?
// match é uma lista de patterns e do que fazer se houver correspondência  
//  
// match EXPR {  
//    PAT => EXPR  
//    PAT => EXPR  
//    ..  
// }  
  
match (a, b) {  
	(5, x) => {  
		// Se (a,b) casar com (5,x), este bloco roda  
	},  
	(x, 5) => {  
		// Da mesma forma: se (a,b) casar com (x, 5)..  
	},  
	(x, y) => {  
		// E este é o pattern que "captura tudo", igual a como `let (x,y) = (a,b)` funciona  
	}  
}  

Vamos causar dor

  • Confundir as pessoas já é divertido, mas e causar miséria completa e bugs reais?
  • Para mim, esta é a sintaxe mais sutil do Rust:
    • A linha mais interessante deste texto: a sintaxe mais sutil do Rust é que constantes por si só são patterns
  • Essa sintaxe adiciona algumas ergonomias legais em torno de matching:
let input: i32 = ..;  
  
const GOOD: i32 = 1;  
const BAD: i32 = 2;  
  
match input {  
	// Isto verifica se input == GOOD, porque GOOD é uma constante  
	GOOD => println!("input was 1"),  
	// Isto verifica se input == BAD, porque BAD é uma constante.  
	BAD => println!("input was 2"),  
	// Isto define otherwise = input e sempre casa...  
	otherwise => println!("input was {otherwise}"),  
}  

Mas escrever constantes em maiúsculas é só convenção. No máximo, há um warning do compilador para não fazer isso.

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

Agora temos três branches que parecem iguais, mas o que fazem depende de existir ou não uma constante com aquele nome!
Vamos piorar. O que acontece abaixo?

const GOOD: i32 = 1;  
match input {  
	// Erro de digitação...  
	GOD => println!("input was 1"),  
	otherwise => println!("input was not 1")  
}  

Aqui o compilador vai emitir um warning, mas esse código sempre vai imprimir input was 1
Ou, de forma mais realista:

// Opa, acidentalmente comentamos ou removemos este import  
// use crate::{SOME_GL_CONSTANT, OTHER_THING}  
  
// Eita!  
match value {  
	SOME_GL_CONSTANT => ..,  
	OTHER_THING => ..,  
	_ => ..,  
}  

Isso confunde as pessoas. Especialmente quando elas tentam fazer coisas elegantes com enums.

enum MyEnum {  
	A, B, C  
}  
  
// Normalmente você escreveria assim  
match value {  
	MyEnum::A => ..,  
	MyEnum::B => ..,  
	MyEnum::C => ..,  
}  
  
// Mas também dá para escrever assim  
use MyEnum::*;  
match value {  
	A => {},  
	B => {},  
	C => {}  
}  
// E então, se você mudar MyEnum...  
enum MyEnum { A, B, D, E };  
use MyEnum::*;  
  
// Isso continua compilando!  
match value {  
	A => {},  
	B => {},  
	C => {},  
}  
  
// `C` agora vira um pattern "captura tudo", porque não existe nada como `C` no escopo.  
// Você está fazendo let C = value, e isso sempre casa!!!  

Clippy tem várias regras que avisam para você não fazer isso, porque isso sempre confunde as pessoas.
Mas dá para deixar ainda mais confuso:

// Vincula x a 5 de forma irrefutável...  
let x = 5;  
  
// ...peraí...  
const x: i32 = 4;  

Esse código não compila. Porque const x é um pattern, constantes sofrem hoisting, e agora esse código é avaliado como:

let 4 = 5;  
  
// error[E0005]: refutable pattern in local binding  
//  --> src/main.rs:3:5  
//   |  
// 3 | let x = 5;  
//   |     ^  
//   |     |  
//   |     os patterns `i32::MIN..=3_i32` e `5_i32..=i32::MAX` não são cobertos  
//   |     os patterns ausentes não são cobertos porque `x` é interpretado como um pattern constante, não como uma nova variável  
//   |     ajuda: introduza uma variável no lugar: `x_var`  
//   |  
//   = nota: bindings `let` exigem um "irrefutable pattern", como um `struct` ou um `enum` com apenas uma variant  

"expr é igual a 4" não é um match irrefutável, e não trata o caso em que isso não acontece

Irritando todo mundo à sua volta

// Suponha que `maybe` seja Option<&str>. Pode ser algum texto, ou None.  
let maybe_username: Option<&str> = ..;  
  
// Este é um pattern comum do Rust para match em uma linha. Se casar com Some(..), podemos fazer algo com essa string.  
if let Some(username) = maybe_username {  
	// Então este código roda se username existir...  
	return username.to_uppercase();  
}  
  
// Mas veja só... agora esse código só roda quando 'username' casar com Some("hey")  
const username: &str = "hey";  

A combinação de hoisting de constantes com o fato de constantes serem patterns permite que você escreva código Rust bem enigmático

Isso não é um problema real

  • Na prática, a única razão de isso poder ser confuso é que você pode escrever let UPPERCASE e const lowercase
  • Se criar variáveis começando com maiúscula fosse um erro de lint, a confusão não aconteceria
    • Porque você não conseguiria acidentalmente fazer binding de algo ao tentar casar com uma variant de enum ou com uma constante
  • Mas, para deixar claro, isso é só uma curiosidade divertida da linguagem
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 comentários

 
sunrabbit 2024-11-05

Na verdade, isso não é um grande problema, porque praticamente todo ambiente de desenvolvimento tem um language server
e ele faz toda a inferência e mostra tudo por ali

O rust-analyzer, que é a base do language server do RustRover, é uma ferramenta bem poderosa

 
sunrabbit 2024-11-05

É basicamente um texto que reúne os dark patterns que existem em qualquer linguagem
para dizer: isto pode causar confusão!

É esse tipo de texto, sabe

 
kayws426 2024-11-05

Nossa... parece complicado. Como será que o Rust pretende lidar com isso?