Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Solutions: 8 — Where there’s one, there’s many

Exercise 1 — The function over a slice

#![allow(unused)]
fn main() {
fn highest_rank_in_hand(hand: &[u32], ranks: &[u8]) -> Option<u8> {
    let mut best: Option<u8> = None;
    for &id in hand {
        let r = ranks[id as usize];
        best = match best {
            None => Some(r),
            Some(b) => Some(b.max(r)),
        };
    }
    best
}

let hand5: Vec<u32>   = vec![3, 17, 21, 28, 41];
let hand1: Vec<u32>   = vec![41];
let hand0: Vec<u32>   = vec![];

println!("{:?}", highest_rank_in_hand(&hand5, &ranks)); // Some(some rank)
println!("{:?}", highest_rank_in_hand(&hand1, &ranks)); // Some(ranks[41])
println!("{:?}", highest_rank_in_hand(&hand0, &ranks)); // None
}

One function. Three N values. The N = 1 and N = 0 cases are not special-cased; they fall out.

Exercise 2 — Reverse the urge

#![allow(unused)]
fn main() {
fn face_cards(ranks: &[u8]) -> Vec<bool> {
    ranks.iter().map(|&r| r >= 10).collect() // 10 = J, 11 = Q, 12 = K (0-indexed)
}
}

Compared to a per-card Card::is_face_card(&self) -> bool plus a loop, the array version is shorter, more cache-friendly, and trivially vectorisable.

Exercise 3 — The N = 0 case

Option<u8> returning None is the cleanest answer. A panic is hostile to callers (the empty case is a valid state of the world — a player just played their last card). A sentinel like 255 confuses the type. Option makes the absence visible at the type level.

Exercise 4 — Singleton as trivial array

#![allow(unused)]
fn main() {
fn red_mask(suits: &[u8]) -> Vec<bool> {
    suits.iter().map(|&s| s < 2).collect() // suits 0,1 = hearts, diamonds
}

let one_suit = 0u8;
let is_red_one = red_mask(&[one_suit])[0]; // true
}

The singleton drops out as a one-element call. There is no separate is_red(suit: u8) -> bool function. If the call site is ergonomic enough you may write a thin wrapper for clarity, but the array version is the canonical implementation.

Exercise 5 — Count overhead

#![allow(unused)]
fn main() {
use std::time::Instant;

let n = 100_000;
let suits: Vec<u8> = (0..n).map(|i| (i % 4) as u8).collect();
let ranks: Vec<u8> = (0..n).map(|i| (i % 13) as u8).collect();

// Per-element loop
let t = Instant::now();
let mut count = 0u64;
for i in 0..n {
    if ranks[i] >= 10 { count += 1; }
}
println!("per-element: {:?}, count = {count}", t.elapsed());

// Array version
let t = Instant::now();
let mask = face_cards(&ranks);
let count: u64 = mask.iter().filter(|&&b| b).count() as u64;
println!("array: {:?}, count = {count}", t.elapsed());
}

Both produce the same count. At 100,000 entries the array version is typically 2-5× faster — partly because the compiler vectorises iter().map().collect() more aggressively than an indexed loop, partly because the predicate is hoisted to the SIMD-friendly form.

The point is not the exact ratio but that the array version has room to be optimised. The per-element version is already at its compiler ceiling.

Exercise 6 — From a tutorial

This is open-ended. The expected outcome: the rewritten array-first version is shorter, has fewer indirections, and answers cross-cutting queries (all face cards on the table; all spades in any hand) in one function call rather than a loop over methods.

A typical OOP card game tutorial weighs around 200-400 lines. The array-first rewrite of the same functionality usually lands at 80-150 lines, with the bulk of the savings coming from not writing getters, setters, copy semantics, and the various small accessors that an OOP Card accumulates.