How does rand::distributions::Uniform::new_inclusive differ from new for range sampling boundaries?
Uniform::new creates a uniform distribution over the half-open range [low, high) where the upper bound is excluded from possible samples, while Uniform::new_inclusive creates a distribution over the closed range [low, high] where the upper bound is included as a possible sample value. The distinction mirrors Rust's range syntax: new(low, high) corresponds to low..high, and new_inclusive(low, high) corresponds to low..=high, making the boundary handling consistent with Rust's range conventions.
Uniform Distribution Basics
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn uniform_basics() {
// Uniform distribution samples values with equal probability
// Within a specified range
// Create a uniform distribution over [0, 10) - exclusive upper
let range = Uniform::new(0, 10);
// Sample from the distribution
let mut rng = thread_rng();
let value: i32 = rng.sample(range);
// Possible values: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
// NOT including 10
println!("Sample: {}", value);
}The Uniform distribution samples integers or floats uniformly within a specified range.
new: Half-Open Range [low, high)
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn new_exclusive() {
// Uniform::new creates [low, high) range
// Upper bound is EXCLUDED from possible samples
let dist = Uniform::new(1, 6); // Dice: 1..6
let mut rng = thread_rng();
// Possible values: 1, 2, 3, 4, 5
// NOT including 6
for _ in 0..10 {
let roll: i32 = rng.sample(dist.clone());
println!("Roll: {}", roll);
assert!(roll >= 1 && roll < 6);
}
// This matches Rust's half-open range: 1..6
// for i in 1..6 { } iterates 1, 2, 3, 4, 5
}new(low, high) creates a distribution that can produce low but never high.
new_inclusive: Closed Range [low, high]
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn new_inclusive_closed() {
// Uniform::new_inclusive creates [low, high] range
// Upper bound is INCLUDED in possible samples
let dist = Uniform::new_inclusive(1, 6); // Dice: 1..=6
let mut rng = thread_rng();
// Possible values: 1, 2, 3, 4, 5, 6
// INCLUDING 6
for _ in 0..10 {
let roll: i32 = rng.sample(dist.clone());
println!("Roll: {}", roll);
assert!(roll >= 1 && roll <= 6);
}
// This matches Rust's inclusive range: 1..=6
// for i in 1..=6 { } iterates 1, 2, 3, 4, 5, 6
}new_inclusive(low, high) creates a distribution that can produce both low and high.
The Range Analogy
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn range_analogy() {
// Rust's range syntax provides a helpful analogy:
// Half-open range (exclusive upper):
// 0..10 -> values 0, 1, 2, ..., 9
for i in 0..10 {
println!("{}", i); // Prints 0-9
}
// Closed range (inclusive upper):
// 0..=10 -> values 0, 1, 2, ..., 10
for i in 0..=10 {
println!("{}", i); // Prints 0-10
}
// Uniform::new matches half-open:
let dist_exclusive = Uniform::new(0, 10); // Like 0..10
// Uniform::new_inclusive matches closed:
let dist_inclusive = Uniform::new_inclusive(0, 10); // Like 0..=10
// Both use the same naming convention as ranges!
}The naming mirrors Rust's .. (exclusive) and ..= (inclusive) range syntax.
Dice Rolling Example
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn dice_rolling() {
let mut rng = thread_rng();
// Standard 6-sided die: values 1 through 6
// We want INCLUSIVE range: 1..=6
// CORRECT: Use new_inclusive for dice
let die = Uniform::new_inclusive(1, 6);
let roll: i32 = rng.sample(die);
println!("Die roll: {}", roll); // 1-6 inclusive
// WRONG: new(1, 6) would only give 1-5
let wrong_die = Uniform::new(1, 6);
let wrong_roll: i32 = rng.sample(wrong_die);
println!("Wrong roll: {}", wrong_roll); // 1-5 only!
// For dice, use new_inclusive because we want all faces
}Dice and games of chance typically need new_inclusive to include all possible outcomes.
Zero-Indexed Collections
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn zero_indexed() {
let items = ['a', 'b', 'c', 'd', 'e'];
// To pick a random element from the array:
// Indices are 0..len (exclusive)
// CORRECT: Use new for zero-indexed selection
let index_dist = Uniform::new(0, items.len());
let mut rng = thread_rng();
let idx: usize = rng.sample(index_dist);
let item = items[idx];
println!("Random item: {}", item);
// This is correct because valid indices are 0..len
// items.len() == 5, valid indices are 0, 1, 2, 3, 4
// WRONG: new_inclusive would include out-of-bounds
let wrong_dist = Uniform::new_inclusive(0, items.len());
// Could produce index 5, which is out of bounds!
}For array indexing and zero-based collections, new(0, len) is correctânew_inclusive could produce out-of-bounds indices.
Floating-Point Ranges
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn floating_point() {
let mut rng = thread_rng();
// Floats also support both range types
// new: [0.0, 1.0) - can produce 0.0, never 1.0
let dist = Uniform::new(0.0, 1.0);
let f: f64 = rng.sample(dist);
assert!(f >= 0.0 && f < 1.0);
// new_inclusive: [0.0, 1.0] - can produce 0.0 AND 1.0
let dist_inc = Uniform::new_inclusive(0.0, 1.0);
let f_inc: f64 = rng.sample(dist_inc);
assert!(f_inc >= 0.0 && f_inc <= 1.0);
// For floats, the difference matters for:
// - Probability calculations where exact 1.0 matters
// - Percentages that should include 100%
// - Angle ranges [0, 2Ď] or [0, 360]
}Floating-point distributions support both range types with the same boundary semantics.
When to Use Each
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn when_to_use() {
// Use new(low, high) for:
// - Zero-indexed array access
// - Loop iteration counts
// - Sizes and counts matching array bounds
// - Any [0, n) range convention
let arr = [1, 2, 3, 4, 5];
let index = Uniform::new(0, arr.len()); // Correct for arrays
// Use new_inclusive(low, high) for:
// - Dice and game outcomes
// - Card values (1-13)
// - Human-indexed ranges (1-10, not 0-9)
// - Inclusive mathematical intervals
let die = Uniform::new_inclusive(1, 6); // Standard die
let card_rank = Uniform::new_inclusive(1, 13); // Ace(1) to King(13)
// | Scenario | Range Type | Method |
// |----------|------------|--------|
// | Array index | [0, len) | new(0, len) |
// | Dice roll | [1, 6] | new_inclusive(1, 6) |
// | Loop count | [0, n) | new(0, n) |
// | Percent 0-100% | [0, 100] | new_inclusive(0, 100) |
}Choose based on whether the upper bound should be a valid outcome.
Common Mistakes
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn common_mistakes() {
let mut rng = thread_rng();
// MISTAKE 1: Using new for dice
let die_wrong = Uniform::new(1, 6); // Only produces 1-5!
// Always get values 1-5, never 6
// Player can never roll a 6!
// MISTAKE 2: Using new_inclusive for array index
let arr = [10, 20, 30, 40, 50];
let idx_wrong = Uniform::new_inclusive(0, arr.len() - 1); // Works but confusing
let idx_right = Uniform::new(0, arr.len()); // Clearer: matches array bounds
// MISTAKE 3: Off-by-one confusion
// Want values 0-9 inclusive? Two ways:
let dist1 = Uniform::new_inclusive(0, 9); // Explicit inclusive
let dist2 = Uniform::new(0, 10); // Half-open, equivalent for integers
// Both produce 0-9 for integers
// But dist2 is idiomatic for zero-indexed ranges
// MISTAKE 4: Float precision edge case
// new_inclusive(0.0, 1.0) can produce exactly 1.0
// new(0.0, 1.0) cannot produce 1.0
// This matters for probabilities and percentages
}The most common mistake is using the wrong range type for the problem domain.
Integer Equivalence
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn integer_equivalence() {
// For integers, these are equivalent:
// Range [a, b] inclusive:
let inclusive = Uniform::new_inclusive(0, 9);
// Range [a, b+1) exclusive:
let exclusive = Uniform::new(0, 10);
// Both produce 0-9 for integers
// Because new(0, 10) excludes 10, leaving 0-9
// Use new_inclusive when you have the exact bounds
// Use new when bounds match array/collection conventions
// Example: Values 1-10
let by_inclusive = Uniform::new_inclusive(1, 10);
let by_exclusive = Uniform::new(1, 11); // Need to add 1
// new_inclusive(1, 10) is clearer: "values 1 through 10"
// new(1, 11) is error-prone: need to remember +1
}For integers, new_inclusive(a, b) and new(a, b+1) are equivalent but new_inclusive is clearer when bounds are natural.
Float Differences Matter
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn float_differences() {
let mut rng = thread_rng();
// For floats, new and new_inclusive are NOT equivalent!
// new(0.0, 1.0): Can produce 0.999999... but NEVER 1.0
let dist_exclusive = Uniform::new(0.0, 1.0);
// Probability of exactly 0.0: very low but possible
// Probability of exactly 1.0: ZERO (excluded)
// new_inclusive(0.0, 1.0): Can produce 1.0
let dist_inclusive = Uniform::new_inclusive(0.0, 1.0);
// Probability of exactly 0.0: very low but possible
// Probability of exactly 1.0: very low but possible
// This matters for:
// - Percentages: 0-100% should include 100%
let percent = Uniform::new_inclusive(0.0, 100.0);
// - Normalized coordinates: [0, 1] should include 1.0
let normalized = Uniform::new_inclusive(0.0, 1.0);
// - But for array indexing with floats, new(0.0, len as f64) may be appropriate
}For floats, the difference between new and new_inclusive is semantically significant.
Sampling Multiple Values
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn sampling_multiple() {
let mut rng = thread_rng();
// Create distribution once, sample many times
let die = Uniform::new_inclusive(1, 6);
// Sample 10 dice rolls
let rolls: Vec<i32> = (0..10)
.map(|_| rng.sample(die.clone()))
.collect();
// Or use sample_iter for convenience
let rolls2: Vec<i32> = rng.sample_iter(die.clone()).take(10).collect();
// Check all values are valid
for roll in &rolls2 {
assert!(*roll >= 1 && *roll <= 6);
}
// The distribution is created once and reused
// More efficient than creating new distribution each sample
}Create the distribution once and sample multiple times for efficiency.
Distribution Creation Performance
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn performance() {
// Both new and new_inclusive have similar creation cost
// For integers: O(1) - simple range setup
// For floats: Slightly more complex
// Uses the algorithm from "Uniform Random Floating-Point Numbers"
// by G. Marsaglia and W. W. Tsang
// Once created, sampling is O(1)
// Create distribution once for repeated sampling:
let dist = Uniform::new_inclusive(1, 100);
let mut rng = thread_rng();
for _ in 0..1_000_000 {
let value: i32 = rng.sample(dist.clone());
// Use value...
}
// Don't create distribution in loop:
// BAD:
// for _ in 0..1_000_000 {
// let dist = Uniform::new_inclusive(1, 100); // Wasteful!
// let value = rng.sample(dist);
// }
}Both methods have similar creation cost; reuse distributions across multiple samples.
The Standard Library Rng::gen_range
use rand::{thread_rng, Rng};
fn gen_range() {
let mut rng = thread_rng();
// Rng::gen_range uses Uniform internally
// It supports both exclusive and inclusive ranges:
// Exclusive: gen_range(low..high)
let excl: i32 = rng.gen_range(0..10); // 0-9
// Inclusive: gen_range(low..=high)
let incl: i32 = rng.gen_range(0..=10); // 0-10
// These use Uniform::new and Uniform::new_inclusive under the hood
// gen_range is convenient for one-off samples
// Uniform is better for repeated sampling
// One-off:
let roll = rng.gen_range(1..=6);
// Repeated:
let die = Uniform::new_inclusive(1, 6);
let rolls: Vec<_> = (0..100).map(|_| rng.sample(die.clone())).collect();
}Rng::gen_range provides a convenient interface using .. and ..= syntax matching Uniform::new and new_inclusive.
Range Types and Bounds Checking
use rand::distributions::Uniform;
use rand::distributions::uniform::UniformInt;
fn bounds_checking() {
// Both methods panic on invalid bounds:
// Empty range: low >= high for new
// let bad = Uniform::new(10, 5); // Panics: range is empty
// Empty range: low > high for new_inclusive
// let bad = Uniform::new_inclusive(10, 5); // Panics
// Single value range works for new_inclusive:
let single = Uniform::new_inclusive(5, 5); // Always returns 5
// But not for new (empty range):
// let bad = Uniform::new(5, 5); // Panics: no values in range
// This difference is important:
// - new_inclusive(a, a) = always sample 'a'
// - new(a, a) = empty range, panic
// For integers:
// - new requires low < high
// - new_inclusive requires low <= high
// For floats:
// - Similar rules, but NaN causes panic
// - Infinity handling depends on implementation
}new panics if low >= high; new_inclusive panics if low > high, allowing single-value distributions.
Single-Value Distributions
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn single_value() {
let mut rng = thread_rng();
// new_inclusive allows single-value distributions
let always_five = Uniform::new_inclusive(5, 5);
let value: i32 = rng.sample(always_five);
assert_eq!(value, 5); // Always 5
// Useful for:
// - Constant parameters in generic code
// - Placeholder distributions
// - Testing with fixed values
// new does NOT allow this:
// let bad = Uniform::new(5, 5); // Panics!
// Workaround for new: add 1 to upper bound
let almost_always_five = Uniform::new(5, 6); // Can produce 5 only
// But this is misleading - use new_inclusive instead
}new_inclusive(a, a) creates a distribution that always returns a; new(a, a) panics.
Practical Decision Guide
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn decision_guide() {
// Decision flow:
// 1. Is the upper bound a valid outcome?
// YES -> Use new_inclusive
// NO -> Use new
// 2. Is this for array indexing?
// YES -> Use new(0, len)
// NO -> Consider natural range
// 3. What's the domain?
// - Game outcomes (dice, cards) -> new_inclusive
// - Array indices -> new
// - Percentages -> new_inclusive(0, 100) or new_inclusive(0.0, 100.0)
// - Coordinates -> depends on convention
// 4. What range syntax matches the intent?
// - "from a up to b" -> new(a, b)
// - "from a through b" -> new_inclusive(a, b)
// Quick reference:
// | Use new | Use new_inclusive |
// |---------|-------------------|
// | Array indices | Dice rolls |
// | Loop counters | Card values |
// | Buffer sizes | Percentages |
// | Offsets | Human ranges (1-N) |
// | [0, n) | [a, b] |
}Choose based on whether the domain naturally includes the upper bound.
Summary Table
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn summary() {
// | Aspect | new(low, high) | new_inclusive(low, high) |
// |--------|----------------|--------------------------|
// | Range | [low, high) | [low, high] |
// | Upper bound | Excluded | Included |
// | Rust analog | low..high | low..=high |
// | Valid bounds | low < high | low <= high |
// | Single value | Panics | Allowed |
// | Typical use | Array indices | Game outcomes |
// Both produce uniform distributions with equal probability
// Both work for integers and floats
// Both have O(1) creation and sampling
// Key insight: Match your mental model of the range
// "0 to 9" (exclusive) -> new(0, 10)
// "1 through 6" (inclusive) -> new_inclusive(1, 6)
}Synthesis
Quick reference:
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
fn quick_reference() {
let mut rng = thread_rng();
// new(low, high) - [low, high) - upper excluded
let exclusive = Uniform::new(0, 10); // Values: 0-9
let index_dist = Uniform::new(0, arr.len()); // Array safe
// new_inclusive(low, high) - [low, high] - upper included
let inclusive = Uniform::new_inclusive(0, 10); // Values: 0-10
let die = Uniform::new_inclusive(1, 6); // Standard die
// Rule of thumb:
// - "up to N" -> new(0, N)
// - "through N" -> new_inclusive(0, N)
// - Array index -> new(0, len)
// - Game outcome -> new_inclusive(min, max)
}Key insight: Uniform::new and Uniform::new_inclusive differ only in their handling of the upper boundânew creates a half-open range [low, high) where high can never be sampled, while new_inclusive creates a closed range [low, high] where high is a valid outcome with equal probability. The distinction mirrors Rust's range syntax (low..high vs low..=high), making the API intuitive for Rust programmers. The practical difference manifests in two common scenarios: for array indexing where valid indices are [0, len), new(0, len) correctly excludes the out-of-bounds index len; for game mechanics like dice where all faces [1, 6] should be possible, new_inclusive(1, 6) correctly includes the maximum value. For integers, new_inclusive(a, b) and new(a, b+1) are mathematically equivalent but convey different intentânew_inclusive is clearer when bounds are naturally inclusive, while new is clearer for zero-indexed collections. For floats, the difference is more significant: new(0.0, 1.0) can never produce exactly 1.0, while new_inclusive(0.0, 1.0) can, which matters for probabilities and percentages that should include the maximum value. The new_inclusive variant also allows single-value distributions (new_inclusive(a, a) always samples a), while new(a, a) panics as an empty range.
