Lewati ke konten

Angka Acak di Rust dengan rand

Game melempar dadu. Kuis mengacak urutan soalnya. Test butuh data contoh. Semuanya butuh angka acak, padahal standard library Rust tidak menyediakan generator angka acak siap pakai. Sebagai gantinya, hampir semua orang memakai satu crate (library Rust): rand.

Halaman ini memakai API modern rand 0.10. Ini penting, karena rand mengganti beberapa nama di versi 0.9 dan 0.10. Kalau sebuah tutorial lama memakai thread_rng() atau gen_range(), berarti tutorial itu ditulis untuk versi lama. Kita akan melihat nama-nama barunya, beserta error yang muncul kalau kamu memakai nama lama.

Di folder proyekmu, jalankan:

Terminal window
cargo add rand

Perintah itu menambahkan baris seperti ini ke Cargo.toml:

[dependencies]
rand = "0.10"

Kalau crate dan Cargo.toml masih baru buatmu, baca halaman itu dulu. Tombol “Coba sendiri” di halaman ini membuka Rust Playground, yang sudah menyertakan rand.

Untuk satu nilai acak sekali pakai, gunakan fungsi bebas rand::random_range dan rand::random:

fn main() {
let die: u32 = rand::random_range(1..=6);
let coin: bool = rand::random();
let chance: f64 = rand::random();
println!("dadu antara 1 dan 6: {}", (1..=6).contains(&die));
println!("chance ada di 0.0..1.0: {}", (0.0..1.0).contains(&chance));
println!("koin keluar angka: {coin}");
}
▶ Coba sendiri

Sengaja tidak ada blok output di sini: baris terakhir bernilai true atau false secara acak. Klik “Coba sendiri” beberapa kali dan lihat hasilnya berubah.

  • random_range(1..=6) memberi angka dari 1 sampai 6, termasuk 6. Dengan 1..6 kamu hanya mendapat 1 sampai 5. Range di sini bekerja sama seperti di perulangan for.
  • random() memilih nilai dengan tipe apa pun yang kamu minta. Untuk bool, hasilnya seperti lempar koin yang adil. Untuk f64, hasilnya angka dari 0.0 sampai (tapi tidak termasuk) 1.0.
  • Anotasi tipe (: u32, : bool) memberi tahu rand apa yang harus dibuat. Tanpa anotasi itu, Rust mungkin tidak tahu tipe mana yang kamu mau.

Kalau kamu butuh banyak nilai acak, ambil sebuah generator sekali saja dengan rand::rng(), lalu panggil method-nya. Cara ini sedikit lebih cepat, dan gaya inilah yang paling sering kamu lihat di kode orang lain:

use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect();
println!("melempar {} dadu", rolls.len());
println!("semua antara 1 dan 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random();
println!("x ada di 0.0..1.0: {}", (0.0..1.0).contains(&x));
}

Output:

melempar 1000 dadu
semua antara 1 dan 6: true
x ada di 0.0..1.0: true
▶ Coba sendiri

Kita mencetak hasil pengecekan, bukan angkanya langsung, supaya output-nya selalu sama walaupun nilainya berubah-ubah.

Dua method lain yang akan sering kamu pakai:

let lucky = rng.random_bool(0.1); // true sekitar 10% dari waktu
let rare = rng.random_ratio(1, 100); // true sekitar 1 dari 100 kali

Kenapa perlu baris use rand::RngExt;? Method seperti random_range dan random_bool berasal dari sebuah trait, dan kamu hanya bisa memanggil method sebuah trait kalau trait itu ada dalam cakupan (scope). Di rand 0.10 ada dua trait:

  • Rng adalah trait dasar “aku bisa menghasilkan bit acak”. Setiap generator mengimplementasikannya.
  • RngExt menambahkan method yang enak dipakai: random, random_range, random_bool, dan lainnya. Trait ini otomatis berlaku untuk setiap Rng.

Jadi: import RngExt untuk memanggil method-nya, dan pakai Rng saat kamu menulis fungsi yang menerima “generator apa saja”. Kamu akan melihat keduanya di contoh pelempar dadu di bawah.

rand::rng() memberi angka yang berbeda setiap kali dijalankan. Itu yang kamu mau di game sungguhan. Tapi untuk test, debugging, atau level “tantangan harian” di sebuah game, kamu justru ingin angka “acak” yang sama setiap kali.

Di sinilah seed berguna. Generator dengan seed selalu menghasilkan urutan yang sama untuk seed yang sama:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
fn main() {
let mut a = StdRng::seed_from_u64(2024);
let mut b = StdRng::seed_from_u64(2024);
let first: Vec<u8> = (0..4).map(|_| a.random_range(0..100)).collect();
let second: Vec<u8> = (0..4).map(|_| b.random_range(0..100)).collect();
println!("{:?}", first);
println!("{:?}", second);
println!("sama? {}", first == second);
}

Output:

[53, 82, 74, 51]
[53, 82, 74, 51]
sama? true
▶ Coba sendiri

seed_from_u64 berasal dari trait SeedableRng, jadi trait itu juga perlu di-import. Ganti 2024 dengan angka lain dan kamu akan mendapat urutan yang berbeda, tapi tetap sama di setiap kali dijalankan.

Satu peringatan jujur: algoritma StdRng boleh berubah di rilis rand berikutnya. Angka di atas berasal dari rand 0.10. Kalau test-mu harus memberi angka yang persis sama selamanya, rand book menyarankan generator yang punya nama tetap, misalnya ChaCha8Rng (lihat “Bacaan lebih lanjut” di bawah).

Untuk bekerja dengan list, import dua trait lagi dari rand::seq:

  • SliceRandom menyediakan shuffle, yang mengacak urutan sebuah slice di tempat.
  • IndexedRandom menyediakan choose (satu item acak) dan sample (beberapa item yang berbeda).
use rand::rngs::StdRng;
use rand::seq::{IndexedRandom, SliceRandom};
use rand::SeedableRng;
fn main() {
let mut rng = StdRng::seed_from_u64(7);
let mut cards = vec!["A", "K", "Q", "J", "10"];
cards.shuffle(&mut rng);
println!("diacak: {:?}", cards);
let colors = ["merah", "hijau", "biru"];
println!("terpilih: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect();
println!("tim: {:?}", team);
let empty: [i32; 0] = [];
println!("dari yang kosong: {:?}", empty.choose(&mut rng));
}

Output:

diacak: ["K", "J", "10", "Q", "A"]
terpilih: Some("merah")
tim: ["K", "J"]
dari yang kosong: None
▶ Coba sendiri

choose mengembalikan Option, karena slice yang kosong tidak punya apa pun untuk dipilih. Method ini bekerja sama baiknya di array, vektor, dan slice. Ganti rng yang memakai seed dengan rand::rng(), dan kamu akan mendapat urutan acak baru di setiap kali dijalankan.

Ayo tulis fungsi yang melempar dadu. Kuncinya ada di tipe parameternya: &mut impl Rng artinya “generator acak apa saja”. Di game sungguhan kamu mengirim rand::rng(). Di sebuah test kamu mengirim StdRng dengan seed dan tahu persis apa hasilnya:

use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng};
/// Lempar `count` dadu, masing-masing dengan `sides` sisi.
fn roll(rng: &mut impl Rng, count: u32, sides: u32) -> Vec<u32> {
(0..count).map(|_| rng.random_range(1..=sides)).collect()
}
fn main() {
let mut rng = StdRng::seed_from_u64(1);
for (count, sides) in [(2, 6), (1, 20), (3, 4)] {
let rolls = roll(&mut rng, count, sides);
let total: u32 = rolls.iter().sum();
println!("{count}d{sides}: {:?} = {total}", rolls);
}
}

Output:

2d6: [5, 6] = 11
1d20: [16] = 16
3d4: [3, 1, 2] = 6
▶ Coba sendiri

2d6 adalah singkatan khas board game untuk “dua dadu bersisi enam”. Mengirim generator sebagai parameter, alih-alih memanggil rand::rng() di dalam roll, adalah kebiasaan yang baik: fungsinya jadi mudah diuji.

Di sini kita memilih 16 karakter dari himpunan karakter yang diizinkan. Himpunan itu sengaja tidak memuat karakter yang mirip satu sama lain seperti 0/O dan 1/l, supaya password-nya lebih mudah diketik:

use rand::seq::IndexedRandom;
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%&*";
fn password(rng: &mut impl Rng, length: usize) -> String {
(0..length)
.map(|_| *CHARSET.choose(rng).unwrap() as char)
.collect()
}
fn main() {
let mut rng = rand::rng();
let pw = password(&mut rng, 16);
println!("panjang: {}", pw.len());
println!("hanya karakter yang diizinkan: {}", pw.bytes().all(|b| CHARSET.contains(&b)));
// println!("{pw}"); // hapus komentar untuk melihatnya: berbeda setiap kali dijalankan
}

Output:

panjang: 16
hanya karakter yang diizinkan: true
▶ Coba sendiri

b"..." adalah byte string: sebuah &[u8] berisi karakter ASCII. choose memilih satu byte, dan as char mengubahnya menjadi karakter. Perhatikan bahwa di sini kita memakai rand::rng(), jangan pernah generator dengan seed. Password dari seed yang tetap akan menjadi password yang sama setiap kali, dan itu sama saja dengan tidak punya password.

Proyek game tebak angka memilih angka rahasianya dengan trik jam, supaya tetap hanya memakai standard library. Dengan rand, seluruh fungsi secret_number cukup jadi satu baris:

let secret: u32 = rand::random_range(1..=100);

Bagian lain dari game tetap sama. Berikut versi yang bisa kamu jalankan di Playground. Angka rahasianya berasal dari generator dengan seed, dan komputer menebak dengan selalu memilih titik tengah dari rentang yang tersisa:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::cmp::Ordering;
fn main() {
// Di game sungguhan: let secret: u32 = rand::random_range(1..=100);
let mut rng = StdRng::seed_from_u64(99);
let secret: u32 = rng.random_range(1..=100);
let (mut low, mut high) = (1, 100);
let mut attempts = 0;
loop {
let guess = (low + high) / 2;
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => { println!("{guess} terlalu kecil"); low = guess + 1; }
Ordering::Greater => { println!("{guess} terlalu besar"); high = guess - 1; }
Ordering::Equal => { println!("{guess} benar! Ketemu dalam {attempts} kali tebakan."); break; }
}
}
}

Output:

50 terlalu kecil
75 terlalu kecil
88 terlalu kecil
94 terlalu kecil
97 terlalu kecil
99 benar! Ketemu dalam 6 kali tebakan.
▶ Coba sendiri

Fakta menarik: strategi “bagi dua rentangnya” ini tidak pernah butuh lebih dari 7 tebakan untuk angka 1 sampai 100.

Apakah rand aman untuk password dan kunci?

Section titled “Apakah rand aman untuk password dan kunci?”

Jawaban singkatnya: rand::rng() dirancang agar sulit ditebak, tapi untuk rahasia yang serius, pakai tool yang memang dibuat untuk itu.

  • rand::rng() memakai algoritma yang aman secara kriptografis (saat ini ChaCha12). Seed-nya diambil dari sumber acak sistem operasi lewat crate getrandom, dan ia memperbarui seed-nya sendiri secara berkala. Dokumentasi rand menyebutnya “fast, reasonably secure generator” (generator yang cepat dan cukup aman), dan mengatakan mereka tidak bisa menjamin generator ini cocok untuk semua kebutuhan keamanan.
  • Untuk kunci enkripsi, token API, dan rahasia penting lainnya, ambil byte-nya langsung dari sistem operasi dengan getrandom (atau rand::rngs::SysRng), atau lebih baik lagi, biarkan library kriptografi khusus yang membuat kuncinya untukmu.
  • Jangan pernah memakai generator dengan seed (seed_from_u64) atau SmallRng yang cepat untuk apa pun yang rahasia. Siapa pun yang tahu atau bisa menebak seed-nya bisa menghasilkan nilai “acak” yang sama.

Untuk game, simulasi, mengacak urutan, dan data test, rand::rng() sudah sangat tepat.

Meng-import Rng, bukan RngExt. Di rand 0.9, method-method ini ada di Rng, jadi kode lama menulis use rand::Rng;. Dengan rand 0.10, hasilnya:

use rand::Rng;
fn main() {
let mut rng = rand::rng();
let n = rng.random_range(1..=6);
println!("{n}");
}
error[E0599]: no method named `random_range` found for struct `ThreadRng` in the current scope
--> src/main.rs:5:17
|
5 | let n = rng.random_range(1..=6);
| ^^^^^^^^^^^^
|
= help: items from traits can only be used if the trait is in scope
help: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it
|
1 + use rand::RngExt;
|

Compiler bilang method random_range tidak ditemukan, karena trait yang menyediakannya belum ada dalam scope. Ia juga langsung memberi tahu solusinya: use rand::RngExt;.

Menyalin kode yang ditulis untuk rand 0.8. Contoh yang sangat lama memakai thread_rng() dan gen_range():

fn main() {
let n = rand::thread_rng().gen_range(1..=6);
println!("{n}");
}
error[E0425]: cannot find function `thread_rng` in crate `rand`
--> src/main.rs:2:19
|
2 | let n = rand::thread_rng().gen_range(1..=6);
| ^^^^^^^^^^ not found in `rand`

Artinya, fungsi thread_rng sudah tidak ada di crate rand. Nama barunya adalah rand::rng() dan random_range(). Begitu juga, gen() menjadi random() dan gen_bool() menjadi random_bool().

Range yang meleset satu. random_range(1..6) tidak akan pernah mengembalikan 6. Untuk dadu, tulis 1..=6.

  • Tambahkan crate-nya dengan cargo add rand. Standard library Rust tidak punya generator angka acak sendiri.
  • Untuk nilai cepat, pakai rand::random_range(1..=6) dan rand::random::<T>().
  • Untuk banyak nilai, pakai let mut rng = rand::rng(); bersama use rand::RngExt; untuk method seperti random_range dan random_bool.
  • SliceRandom::shuffle mengacak urutan slice; IndexedRandom::choose memilih satu item dan mengembalikan Option.
  • StdRng::seed_from_u64(n) memberi urutan yang sama setiap kali dijalankan: cocok untuk test dan level game yang bisa diulang, tapi jangan untuk rahasia.
  • Untuk kunci dan token, pakai sumber acak dari OS (getrandom) atau crate kriptografi khusus.

Kuis singkat

1. Pemanggilan mana yang bisa mengembalikan 6?

2. Dengan rand 0.10, rng.random_range(..) gagal dengan "no method named random_range". Apa yang kurang?

3. Kenapa kamu memakai StdRng::seed_from_u64(42)?

4. Apa yang dikembalikan choose kalau dipanggil pada slice kosong?

Skor: 0 / 4