๐Ÿฆ€ Functional Rust

023: Random Select

Difficulty: 1 Level: Beginner Extract N randomly chosen elements from a list without replacement.

The Problem This Solves

You need to pick random items from a collection โ€” shuffle a playlist, sample test data, deal cards to players. Python programmers reach for `random.sample(my_list, n)`. This example shows how to build the same thing from scratch in Rust, and why the naive approach (just pick `index % length`) produces biased results. The modulo bias problem is subtle: if you have 5 items and your random number is in `[0, 256)`, then indices 0โ€“1 appear 52 times while indices 2โ€“4 appear only 51 times. Over millions of draws, this skew matters in simulations, games, and cryptography. In production you'd use the `rand` crate. Here we build a minimal LCG (Linear Congruential Generator) โ€” the same algorithm used in most C standard libraries โ€” so the code is self-contained and the mechanics are visible.

The Intuition

An LCG (Linear Congruential Generator) is a pseudo-random number sequence produced by the formula: `next = (prev ร— multiplier + increment) mod 2^64`. The magic constants (from Numerical Recipes) are chosen so the sequence visits all 2^64 values before repeating. We discard the low bits (they have poor randomness) and use only the top 31 bits. Sampling without replacement means once you pick an item, it can't be picked again. The cleanest way: copy the list into a mutable pool, pick a random index, remove that element (so it shrinks), repeat. This is the Fisher-Yates shuffle truncated to N steps. With replacement: each pick is independent โ€” re-index the full list every time. Useful when you want duplicates (e.g., bootstrap statistics).

How It Works in Rust

struct Lcg { state: u64 }

impl Lcg {
 fn new(seed: u64) -> Self { Lcg { state: seed } }

 fn next_usize(&mut self, modulus: usize) -> usize {
     // LCG step: multiply + add (wrapping to stay in u64)
     self.state = self.state
         .wrapping_mul(6364136223846793005)
         .wrapping_add(1442695040888963407);
     // Use top 31 bits โ€” they have better statistical quality than low bits
     ((self.state >> 33) as usize) % modulus
 }
}
Sampling without replacement:
fn rand_select<T: Clone>(lst: &[T], n: usize, seed: u64) -> Vec<T> {
 let mut rng = Lcg::new(seed);
 let mut pool = lst.to_vec();          // mutable working copy
 let count = n.min(pool.len());        // clamp to available items
 let mut result = Vec::with_capacity(count);

 for _ in 0..count {
     let idx = rng.next_usize(pool.len());
     result.push(pool.remove(idx));    // remove shrinks the pool
 }
 result
}
`pool.remove(idx)` does two things: returns the element and closes the gap, so subsequent picks never re-select it.

What This Unlocks

Key Differences

ConceptOCamlRust
Random int`Random.int n` (global state, no seed needed)Custom LCG or `rand::thread_rng().gen_range(0..n)`
Remove from list`List.filteri` or recursive rebuild (O(n))`Vec::remove(idx)` โ€” shifts elements (also O(n))
Seeded determinism`Random.init seed` (global)Seed stored in struct โ€” no global state
Bias avoidanceSame modulo bias issue existsSame; `rand` crate uses rejection sampling
// Random Select โ€” 99 Problems #23
// Extract n randomly selected elements from a list.
// Uses a simple LCG pseudo-random generator (no external crates).

/// A minimal LCG pseudo-random number generator.
struct Lcg {
    state: u64,
}

impl Lcg {
    fn new(seed: u64) -> Self {
        Lcg { state: seed }
    }

    /// Returns next value in [0, modulus).
    fn next_usize(&mut self, modulus: usize) -> usize {
        // LCG parameters from Numerical Recipes
        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
        ((self.state >> 33) as usize) % modulus
    }
}

/// Select n random elements (without replacement) using LCG.
fn rand_select<T: Clone>(lst: &[T], n: usize, seed: u64) -> Vec<T> {
    let mut rng = Lcg::new(seed);
    let mut pool = lst.to_vec();
    let count = n.min(pool.len());
    let mut result = Vec::with_capacity(count);

    for _ in 0..count {
        let idx = rng.next_usize(pool.len());
        result.push(pool.remove(idx));
    }
    result
}

/// Select with replacement.
fn rand_select_with_replacement<T: Clone>(lst: &[T], n: usize, seed: u64) -> Vec<T> {
    if lst.is_empty() {
        return vec![];
    }
    let mut rng = Lcg::new(seed);
    (0..n).map(|_| lst[rng.next_usize(lst.len())].clone()).collect()
}

fn main() {
    let input = vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
    println!("Input: {:?}", input);

    let selected = rand_select(&input, 3, 42);
    println!("Random select 3 (seed=42):  {:?}", selected);

    let selected2 = rand_select(&input, 3, 99);
    println!("Random select 3 (seed=99):  {:?}", selected2);

    let with_rep = rand_select_with_replacement(&input, 5, 42);
    println!("With replacement 5 (seed=42): {:?}", with_rep);

    // Deterministic: same seed always gives same result
    assert_eq!(rand_select(&input, 3, 42), rand_select(&input, 3, 42));
    println!("Deterministic: โœ“");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_deterministic() {
        let input = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        let r1 = rand_select(&input, 3, 42);
        let r2 = rand_select(&input, 3, 42);
        assert_eq!(r1, r2);
    }

    #[test]
    fn test_correct_count() {
        let input = vec!['a', 'b', 'c', 'd', 'e'];
        let selected = rand_select(&input, 3, 7);
        assert_eq!(selected.len(), 3);
    }

    #[test]
    fn test_no_duplicates_without_replacement() {
        let input = vec![1, 2, 3, 4, 5];
        let selected = rand_select(&input, 5, 123);
        let mut sorted = selected.clone();
        sorted.sort();
        sorted.dedup();
        assert_eq!(sorted.len(), 5);
    }

    #[test]
    fn test_select_more_than_available() {
        let input = vec![1, 2, 3];
        // Clamps to list length
        assert_eq!(rand_select(&input, 10, 1).len(), 3);
    }
}
(* Random Select *)
(* OCaml 99 Problems #23 *)

(* Implementation for example 23 *)

(* Tests *)
let () =
  (* Add tests *)
  print_endline "โœ“ OCaml tests passed"