14 poin oleh xguru 2024-11-05 | 3 komentar | Bagikan ke WhatsApp

let dan const di Rust

  • let digunakan untuk mendeklarasikan variabel baru
    • Berbentuk let PAT = EXPR;, dan lebih kuat daripada yang terlihat
    • Digabungkan dengan pattern matching, ini memberikan fitur yang praktis
      • let (a, b) = (5, 10);
      • let maybe_string: Option<String> = ..;
      • let Some(value) = maybe_string else { panic!("die horribly")};
  • const adalah konstanta yang dihitung saat compile time dan langsung disisipkan ke dalam kode hasil kompilasi
    • const MY_VAR: &str = "heyyyyyyyy man"; const SECRET: i32 = 0x1234;
    • Berbentuk const IDENT: TYPE = EXPR;, tipe harus ditulis eksplisit dan pola tidak bisa digunakan

Bagian yang membingungkan

  • const bisa digunakan tanpa memedulikan urutan deklarasi (hoisting)
// Tetap dikompilasi walaupun X didefinisikan setelah Y  
const Y: i32 = X + X;  
const X: i32 = 5;  
  • Bisa juga dideklarasikan di dalam fungsi, dan tetap mengalami hoisting
fn oh_boy() -> i32 {  
	return X;  
	const X: i32 = 5;  
	// ^ ini terkompilasi dan berjalan. Tanpa warning!  
}  
  • Jika Anda bekerja dengan programmer yang baru belajar Rust setelah datang dari JavaScript, fitur ini sangat efektif untuk membuat mereka kebingungan
  • Itu konsekuensi yang tidak berbahaya dari sebuah fitur bagus, jadi sekarang mari kita tulis konsekuensi yang berbahaya

match di Rust

// let PAT = EXPR;  
let x = 5;  
  
// Dalam kasus ini, `x` adalah sebuah pattern. Dicek apakah `5` bisa dimasukkan ke `x`  
// Pattern ini selalu match -- Anda selalu bisa memasukkan 5 ke variabel bernama `x`   
  
// Tidak semua pattern harus selalu match. Misalnya:  
let (5, x) = (a, b);  
// Di sini ekspresinya hanya "match" dengan pattern jika a == 5  
//  
// Ini disebut pattern yang "refutable"  
//  
// Dalam deklarasi `let`, pattern yang refutable harus menangani kasus saat "ditolak":  
let (5, x) = (a, b) else { panic!() };  
//  
// ...kalau tidak, Anda bisa punya variabel yang "hanya ada secara kondisional", dan itu bukan hal yang baik  
  • Jadi mari kita lihat match. Apa itu match?
// match adalah daftar pattern dan apa yang harus dilakukan jika cocok  
//  
// match EXPR {  
//    PAT => EXPR  
//    PAT => EXPR  
//    ..  
// }  
  
match (a, b) {  
	(5, x) => {  
		// Jika (a,b) match dengan (5,x), blok ini dijalankan  
	},  
	(x, 5) => {  
		// Sama seperti di atas: jika (a,b) match dengan (x, 5), maka..  
	},  
	(x, y) => {  
		// Dan ini adalah pattern "menangkap semuanya", sama seperti cara kerja let (x,y) = (a,b)  
	}  
}  

Mari menimbulkan penderitaan

  • Membingungkan orang memang menyenangkan, tapi bagaimana kalau menyebabkan kesengsaraan total dan bug sungguhan?
  • Menurut saya, inilah sintaks Rust yang paling sulit disadari:
    • Satu baris paling menarik dalam tulisan ini: Sintaks Rust yang paling sulit disadari adalah bahwa konstanta itu sendiri adalah pattern
  • Sintaks ini menambahkan beberapa ergonomic yang bagus di sekitar matching:
let input: i32 = ..;  
  
const GOOD: i32 = 1;  
const BAD: i32 = 2;  
  
match input {  
	// Ini memeriksa apakah input == GOOD, karena GOOD adalah konstanta  
	GOOD => println!("input was 1"),  
	// Ini memeriksa apakah input == BAD, karena BAD adalah konstanta.  
	BAD => println!("input was 2"),  
	// Ini mendefinisikan otherwise = input, dan selalu match...  
	otherwise => println!("input was {otherwise}"),  
}  

Namun, menulis konstanta dengan huruf besar hanyalah konvensi. Compiler hanya memberi warning jika Anda tidak melakukannya.

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

Sekarang kita punya tiga cabang yang tampak sama, tetapi perilakunya bergantung pada apakah konstanta dengan nama itu ada atau tidak!
Mari kita buat lebih buruk lagi. Apa yang akan terjadi di bawah ini?

const GOOD: i32 = 1;  
match input {  
	// Typo...  
	GOD => println!("input was 1"),  
	otherwise => println!("input was not 1")  
}  

Di sini compiler akan memberi warning, tetapi kode ini akan selalu mencetak input was 1
Atau, secara lebih realistis:

// Waduh, import ini tidak sengaja dikomentari atau dihapus  
// use crate::{SOME_GL_CONSTANT, OTHER_THING}  
  
// Waduh!  
match value {  
	SOME_GL_CONSTANT => ..,  
	OTHER_THING => ..,  
	_ => ..,  
}  

Ini memang membingungkan orang. Terutama saat mereka mencoba melakukan hal-hal keren dengan enum.

enum MyEnum {  
	A, B, C  
}  
  
// Biasanya ditulis seperti ini  
match value {  
	MyEnum::A => ..,  
	MyEnum::B => ..,  
	MyEnum::C => ..,  
}  
  
// Tapi Anda juga bisa menulis seperti ini  
use MyEnum::*;  
match value {  
	A => {},  
	B => {},  
	C => {}  
}  
// Lalu, jika MyEnum diubah...  
enum MyEnum { A, B, D, E };  
use MyEnum::*;  
  
// Ini masih tetap terkompilasi!  
match value {  
	A => {},  
	B => {},  
	C => {},  
}  
  
// `C` sekarang menjadi pattern "menangkap semuanya", karena tidak ada hal bernama `C` yang ada dalam scope.  
// Anda sebenarnya sedang melakukan let C = value, dan itu selalu match!!!  

Clippy punya banyak aturan yang memberi warning agar Anda tidak melakukan ini, karena ini memang terus-menerus membingungkan orang.
Tapi ini bisa dibuat lebih membingungkan lagi:

// Mengikat x ke 5 secara irrefutable...  
let x = 5;  
  
// ...eh tunggu...  
const x: i32 = 4;  

Kode ini tidak akan terkompilasi. Karena const x adalah pattern, konstanta mengalami hoisting, dan sekarang kode ini dievaluasi seperti berikut:

let 4 = 5;  
  
// error[E0005]: refutable pattern in local binding  
//  --> src/main.rs:3:5  
//   |  
// 3 | let x = 5;  
//   |     ^  
//   |     |  
//   |     pattern `i32::MIN..=3_i32` dan `5_i32..=i32::MAX` tidak tercakup  
//   |     pattern yang hilang tidak tercakup karena `x` ditafsirkan sebagai constant pattern, bukan variabel baru  
//   |     bantuan: perkenalkan variabel sebagai gantinya: `x_var`  
//   |  
//   = catatan: binding `let` membutuhkan "irrefutable pattern", seperti `struct` atau `enum` dengan hanya satu variant  

"expr sama dengan 4" bukanlah match yang tidak bisa dibantah, dan tidak menangani kasus selain itu

Membuat semua orang di sekitar kesal

// Anggap `maybe` bertipe Option<&str>. Bisa berisi teks, atau None.  
let maybe_username: Option<&str> = ..;  
  
// Ini pattern Rust yang umum untuk match satu baris. Jika ini match dengan Some(..), kita bisa melakukan sesuatu dengan string itu.  
if let Some(username) = maybe_username {  
	// Jadi kode ini akan dijalankan jika username ada...  
	return username.to_uppercase();  
}  
  
// Tapi begini... sekarang kode itu hanya dijalankan saat 'username' match dengan Some("hey")  
const username: &str = "hey";  

Kombinasi hoisting konstanta dan fakta bahwa konstanta adalah pattern memungkinkan Anda menulis kode Rust yang terasa seperti teka-teki

Ini sebenarnya bukan masalah nyata

  • Secara realistis, satu-satunya alasan ini bisa membingungkan adalah karena Anda bisa menulis let UPPERCASE dan const lowercase
  • Jika membuat variabel yang diawali huruf besar adalah lint error, kebingungan ini tidak akan terjadi
    • Karena Anda tidak akan bisa tanpa sengaja melakukan binding saat sebenarnya ingin match dengan variant enum atau konstanta
  • Tapi agar jelas, ini hanyalah keanehan bahasa yang lucu
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 komentar

 
sunrabbit 2024-11-05

Sebenarnya ini bukan masalah besar, karena di hampir semua lingkungan pengembangan ada language server
dan semuanya diinferensikan lalu ditampilkan dari sana

rust-analyzer, yang menjadi fondasi language server di RustRover, adalah alat yang cukup kuat

 
sunrabbit 2024-11-05

Cuma mengumpulkan dark pattern yang ada di bahasa mana pun
Ini bisa menimbulkan kebingungan!

Kurang lebih artikel dengan nuansa seperti itu, kan

 
kayws426 2024-11-05

Wah... terasa agak begitu ya. Kira-kira bagaimana rencana Rust menanganinya?