A sintaxe mais sutil do Rust
(zkrising.com)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")};
- Na forma
constsão constantes avaliadas em tempo de compilação e incluídas diretamente no código compiladoconst 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
constpode 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 UPPERCASEeconst 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
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É 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
Nossa... parece complicado. Como será que o Rust pretende lidar com isso?