#183 Jun 2026

183. std::mem::take — Move Out of &mut self Without the Clone

You need the Vec out of &mut self, the borrow checker says no, so you .clone() it. Don’t. mem::take swaps in the default and hands you the original — zero allocation.

The borrow checker won’t let you move a field out of &mut self, because that would leave self half-initialized. The usual workarounds are ugly: clone the whole thing, or refactor the API to take self by value.

std::mem::take does the right thing in one line. It replaces the field with T::default() and returns the old value. For collections, Default is empty — so there’s no allocation, just a pointer swap.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::mem;

struct Buffer {
    items: Vec<String>,
}

impl Buffer {
    fn drain_all(&mut self) -> Vec<String> {
        // self.items                 // ❌ cannot move out of borrowed content
        // self.items.clone()         // ❌ allocates + copies every String
        mem::take(&mut self.items)    // ✅ swaps in empty Vec, returns the real one
    }
}

Where it really earns its keep is state-machine transitions, where you need to consume the data inside the current variant before swapping the variant:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::mem;

enum Stream {
    Buffering(Vec<u8>),
    Done,
}

impl Stream {
    fn finish(&mut self) -> Vec<u8> {
        if let Stream::Buffering(buf) = self {
            let bytes = mem::take(buf);   // pull the Vec out, leave [] behind
            *self = Stream::Done;          // now safe to overwrite self
            return bytes;
        }
        Vec::new()
    }
}

Without mem::take, that pattern usually devolves into a mem::replace(self, Stream::Done) and a match on the returned value. mem::take is shorter and reads top-to-bottom.

It works for any T: DefaultString, HashMap, Option, Box<[T]>, your own structs that derive Default. If Default isn’t free for your type, reach for mem::replace and pass the sentinel you actually want.

#182 Jun 2026

182. Path::with_extension — Swap a File Extension Without Slicing Strings

You have report.txt and want report.md. Reaching for replace(".txt", ".md") or a rfind('.')? Stop — Path::with_extension returns a fresh PathBuf with the extension swapped, and it gets every edge case right.

The string-slicing trap

The naïve fix looks reasonable until you read it carefully:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn change_ext_bad(name: &str, ext: &str) -> String {
    match name.rfind('.') {
        Some(i) => format!("{}.{}", &name[..i], ext),
        None    => format!("{}.{}", name, ext),
    }
}

assert_eq!(change_ext_bad("report.txt", "md"), "report.md");
// But...
assert_eq!(change_ext_bad("./.bashrc", "bak"), "./.bak"); // ate the dotfile name

That second case is the bug: ./.bashrc has no extension — the leading dot is part of the name. Manual rfind('.') doesn’t know that.

The fix: Path::with_extension

1
2
3
4
5
6
use std::path::{Path, PathBuf};

let p = Path::new("reports/q1.txt");
let renamed: PathBuf = p.with_extension("md");

assert_eq!(renamed, PathBuf::from("reports/q1.md"));

It returns a new PathBuf — original path untouched — and stays in OsStr land the whole way through, so non-UTF-8 paths survive intact.

Dotfiles are handled the way you’d want:

1
2
3
4
use std::path::{Path, PathBuf};

assert_eq!(Path::new(".bashrc").with_extension("bak"),
           PathBuf::from(".bashrc.bak"));

No extension to start? It adds one instead of failing:

1
2
3
4
use std::path::{Path, PathBuf};

assert_eq!(Path::new("Makefile").with_extension("bak"),
           PathBuf::from("Makefile.bak"));

Pass "" to strip the extension

The same method, with an empty string, removes the extension entirely — no separate without_extension API needed:

1
2
3
4
use std::path::{Path, PathBuf};

let src = Path::new("build/main.o");
assert_eq!(src.with_extension(""), PathBuf::from("build/main"));

Common pattern in build scripts: derive an output path from an input path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::path::{Path, PathBuf};

fn object_for(src: &Path) -> PathBuf {
    src.with_extension("o")
}

assert_eq!(object_for(Path::new("src/main.rs")),
           PathBuf::from("src/main.o"));
assert_eq!(object_for(Path::new("src/lib.rs")),
           PathBuf::from("src/lib.o"));

Only the last extension changes

with_extension replaces from the last dot — same rule as file_stem. For archive.tar.gz, that means only .gz gets swapped:

1
2
3
4
use std::path::{Path, PathBuf};

assert_eq!(Path::new("archive.tar.gz").with_extension("zst"),
           PathBuf::from("archive.tar.zst"));

That’s almost always what you want for compression tools. If you need to strip the whole .tar.gz and start over, call with_extension("") twice — or reach for file_prefix (see bite 116).

set_extension if you already own the PathBuf

The mutating sibling lives on PathBuf and avoids the allocation when you already own the path:

1
2
3
4
5
use std::path::PathBuf;

let mut p = PathBuf::from("notes/draft.md");
p.set_extension("html");
assert_eq!(p, PathBuf::from("notes/draft.html"));

Returns booltrue if the extension was set, false if the path had no file name to attach one to. Most callers ignore it.

Reach for with_extension (or set_extension) any time you’d otherwise write a rfind('.') or a replace(".old", ".new"). It’s been stable since Rust 1.0 — there’s no excuse left.

#181 Jun 2026

181. Option::get_or_insert_with — Lazy Default That Returns &mut

You have an Option<Vec<T>> field and want to push to it. If it’s None, allocate first; if it’s Some, just push. get_or_insert_with does both in one call — and hands you back a &mut so you can use it on the same line.

The dance you don’t have to do

The naïve version checks, assigns, then unwraps:

1
2
3
4
5
6
7
8
let mut tags: Option<Vec<String>> = None;

if tags.is_none() {
    tags = Some(Vec::new());
}
tags.as_mut().unwrap().push("verbose".into());

assert_eq!(tags, Some(vec!["verbose".to_string()]));

Three lines, an unwrap, and you re-borrow the Option twice. get_or_insert_with collapses it:

1
2
3
4
5
let mut tags: Option<Vec<String>> = None;

tags.get_or_insert_with(Vec::new).push("clean".into());

assert_eq!(tags, Some(vec!["clean".to_string()]));

It initializes the Option to Some(f()) if and only if it was None, and returns &mut T to the inner value either way. No unwrap, no double-check.

The closure only runs when needed

That’s the whole point of the _with suffix: the default is lazy. If the value’s already there, your closure never fires, which matters when the default is expensive or has side effects:

1
2
3
4
5
6
7
8
let mut cache: Option<Vec<u8>> = Some(vec![1, 2, 3]);

let buf = cache.get_or_insert_with(|| {
    panic!("would allocate a huge buffer");
});
buf.push(4);

assert_eq!(cache, Some(vec![1, 2, 3, 4]));

If your default is cheap (a String::new(), a 0u32), use the eager sibling get_or_insert and skip the closure:

1
2
3
let mut log: Option<String> = None;
log.get_or_insert(String::new()).push_str("hi");
assert_eq!(log.as_deref(), Some("hi"));

Why the &mut return matters

get_or_insert_with returns &mut T, not T or Option<T>. That lets you keep chaining — push, mutate, hand to another function — without ever re-borrowing the Option:

1
2
3
4
5
6
let mut counters: Option<Vec<u32>> = None;

counters.get_or_insert_with(Vec::new).extend([1, 2, 3]);
counters.get_or_insert_with(Vec::new).push(4);

assert_eq!(counters, Some(vec![1, 2, 3, 4]));

The classic case is builders and config structs with Option<Vec<_>> fields that should only allocate when the caller actually adds something. One line per add, no upfront Some(Vec::new()), no unwrap.

Stable since Rust 1.20.

#180 Jun 2026

180. Option::unzip — Split an Optional Pair Into a Pair of Options

When you have an Option<(A, B)> but the rest of your code wants (Option<A>, Option<B>), the obvious move is two map calls. Option::unzip does both halves in a single call.

The two-map dance

Say a parser returns Option<(&str, i32)> — a name and an age, or nothing. Downstream you want them as separate optional columns:

1
2
3
4
5
6
7
let parsed: Option<(&str, i32)> = Some(("ada", 36));

let name = parsed.map(|(n, _)| n);
let age  = parsed.map(|(_, a)| a);

assert_eq!(name, Some("ada"));
assert_eq!(age,  Some(36));

That’s two passes over the same Option, two closures that throw away half their input, and an easy place to typo n for a. Option::unzip collapses it:

1
2
3
4
5
6
let parsed: Option<(&str, i32)> = Some(("ada", 36));

let (name, age) = parsed.unzip();

assert_eq!(name, Some("ada"));
assert_eq!(age,  Some(36));

Some((a, b)) becomes (Some(a), Some(b)). No closures, no destructuring noise.

The None case is the whole point

unzip shines on the None branch. None becomes (None, None) — both sides empty, no special-casing:

1
2
3
4
5
let missing: Option<(&str, i32)> = None;
let (name, age) = missing.unzip();

assert_eq!(name, None);
assert_eq!(age,  None);

Compare with the manual version: you’d still write two maps and rely on each one to short-circuit. Same behavior, more lines, more chances to drift apart when someone edits one branch and forgets the other.

Round-trip with zip

Option::zip is the inverse — it builds Option<(A, B)> from (Option<A>, Option<B>), returning Some only if both are Some:

1
2
3
4
5
6
7
8
9
let name: Option<&str> = Some("ada");
let age: Option<i32>   = Some(36);

let joined = name.zip(age);
assert_eq!(joined, Some(("ada", 36)));

let (n, a) = joined.unzip();
assert_eq!(n, Some("ada"));
assert_eq!(a, Some(36));

One mental model: Option<(A, B)> and (Option<A>, Option<B>) are almost the same shape, and these two methods let you flip between them without match.

When to reach for it

Any time a single optional value naturally carries two parts that the rest of the code wants to handle independently — coordinates, key/value pairs, name/age, parsed/raw. If you find yourself writing opt.map(|(x, _)| x) and opt.map(|(_, y)| y) back-to-back, that’s the signal.

Stable since Rust 1.66.

#179 Jun 2026

179. Iterator::max_by_key — Find the Best Element Without a Manual Fold

Finding the “best” item in a collection — longest string, heaviest order, latest timestamp — is one of those tasks where a hand-rolled fold keeps showing up. Iterator::max_by_key does the same job in one call, and min_by_key is right there next to it.

The fold you keep writing

You have a slice of strings and want the longest one. The DIY version looks something like this:

1
2
3
4
5
6
7
8
9
let words = ["pear", "raspberry", "fig", "kiwi"];

let longest = words.iter().fold(None, |best, w| match best {
    None => Some(w),
    Some(b) if w.len() > b.len() => Some(w),
    other => other,
});

assert_eq!(longest, Some(&"raspberry"));

That’s a lot of code for a one-liner concept. max_by_key collapses the whole pattern:

1
2
3
let words = ["pear", "raspberry", "fig", "kiwi"];
let longest = words.iter().max_by_key(|w| w.len());
assert_eq!(longest, Some(&"raspberry"));

You hand it a closure that extracts the key — the thing you want to maximise — and it returns the item that produced the largest key.

Works on anything Ord

The key doesn’t have to be a number. It just has to implement Ord, which means strings, tuples, dates, your own types — anything totally ordered:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[derive(Debug, PartialEq)]
struct Order { id: u32, total_cents: u64 }

let orders = vec![
    Order { id: 1, total_cents: 1299 },
    Order { id: 2, total_cents: 4500 },
    Order { id: 3, total_cents: 800 },
];

let biggest = orders.iter().max_by_key(|o| o.total_cents);
assert_eq!(biggest, Some(&Order { id: 2, total_cents: 4500 }));

Tuples are where this really shines — you get multi-key sorting for free, because (A, B): Ord compares lexicographically:

1
2
3
4
5
let words = ["pear", "fig", "kiwi", "lime"];

// Longest word, then alphabetically last among ties.
let pick = words.iter().max_by_key(|w| (w.len(), *w));
assert_eq!(pick, Some(&"pear"));

Same trick with min_by_key — the entire _by_key family follows the same shape.

The tie-break rule, and why it matters

When two elements produce equal keys, max_by_key returns the last one and min_by_key returns the first. That asymmetry is in the docs and it bites people:

1
2
3
4
5
6
7
let nums = [3, 1, 4, 1, 5, 9, 2, 6, 5];

let max = nums.iter().max_by_key(|&&n| n);
let min = nums.iter().min_by_key(|&&n| n);

assert_eq!(max, Some(&9));
assert_eq!(min, Some(&1)); // the first 1, not the second

If you need a specific tie-break — “longest word, but earliest in the list when tied” — encode it in the key itself with a tuple, instead of relying on iteration order.

Floats need min_by / max_by

f32 and f64 don’t implement Ord (because NaN), so max_by_key won’t compile if your key is a float. Reach for max_by and pass a comparator instead:

1
2
3
4
5
6
let measurements = [1.2_f64, 3.4, 0.5, 2.8];

let biggest = measurements.iter()
    .max_by(|a, b| a.partial_cmp(b).unwrap());

assert_eq!(biggest, Some(&3.4));

Or wrap your floats in something like ordered_float::OrderedFloat and stay with max_by_key.

Takeaway

Any time you find yourself folding to track the “best so far,” check whether max_by_key or min_by_key says it in one line. The closure extracts the score; the iterator returns the winner. For ties you control the rule by shaping the key.

#178 Jun 2026

178. Ord::clamp — Stop Writing min(max, max(min, x))

Bounding a value between two limits is one of those tiny operations where everyone hand-rolls a confusing nest of min/max calls. Ord::clamp is the single-call version, and it works on anything that’s Ord or PartialOrd — not just numbers.

You’ve seen this somewhere in every codebase:

1
2
let volume = raw.max(0).min(100);          // OK
let other  = std::cmp::min(100, std::cmp::max(0, raw)); // worse

Both work. Both make you stop and squint to figure out which bound is which. clamp says exactly what it does:

1
let volume = raw.clamp(0, 100);

It’s defined for Ord on integers, and as f32::clamp / f64::clamp for floats (which need PartialOrd because of NaN). Same shape on all of them:

1
2
3
4
5
6
assert_eq!((-5_i32).clamp(0, 10), 0);
assert_eq!(7_i32.clamp(0, 10), 7);
assert_eq!(99_i32.clamp(0, 10), 10);

assert_eq!(2.5_f64.clamp(0.0, 1.0), 1.0);
assert_eq!((-0.3_f64).clamp(0.0, 1.0), 0.0);

It’s not just for numbers. Anything Ord works — char, &str, String, your own types:

1
2
assert_eq!('z'.clamp('a', 'f'), 'f');
assert_eq!("zebra".clamp("apple", "mango"), "mango");

One gotcha: clamp panics if min > max. That’s deliberate — a backwards range is almost always a bug, and silently returning either bound would hide it. If your bounds come from user input or config, validate them once at the boundary:

1
2
3
4
5
6
fn safe_clamp(x: i32, lo: i32, hi: i32) -> i32 {
    let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
    x.clamp(lo, hi)
}

assert_eq!(safe_clamp(5, 10, 0), 5); // bounds got swapped, still works

For floats there’s one more wrinkle: f64::clamp propagates NaN if the input is NaN, but panics if either bound is NaN. So x.clamp(0.0, 1.0) is safe as long as your bounds are real numbers — which they always should be.

#177 Jun 2026

177. BTreeMap::range — Iterate a Sorted Map by Key Range

You have a sorted map keyed by time, port number, or version, and you want every entry between two keys. Filtering on iter() walks the whole map — BTreeMap::range walks just the slice you asked for.

The filter-everything pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::collections::BTreeMap;

let mut map = BTreeMap::new();
map.insert(1, "a");
map.insert(3, "c");
map.insert(5, "e");
map.insert(7, "g");

// Walk every entry, skip the ones you don't want.
let hits: Vec<_> = map.iter().filter(|(k, _)| (3..7).contains(*k)).collect();
assert_eq!(hits, vec![(&3, &"c"), (&5, &"e")]);

Correct, but with a million entries you still traversed all of them. BTreeMap::range takes a RangeBounds and seeks straight to the start key, iterating only through the requested window:

1
2
3
4
5
6
7
use std::collections::BTreeMap;

let map: BTreeMap<i32, &str> =
    BTreeMap::from([(1, "a"), (3, "c"), (5, "e"), (7, "g")]);

let hits: Vec<_> = map.range(3..7).collect();
assert_eq!(hits, vec![(&3, &"c"), (&5, &"e")]);

Same result, log-N lookup to the first key, in-order iteration from there.

All the usual range syntax works

Any RangeBounds<K> is fair game — .., a..b, a..=b, ..b, a..:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::collections::BTreeMap;

let map: BTreeMap<i32, &str> =
    BTreeMap::from([(1, "a"), (3, "c"), (5, "e"), (7, "g")]);

let inclusive: Vec<_> = map.range(3..=7).collect();
assert_eq!(inclusive, vec![(&3, &"c"), (&5, &"e"), (&7, &"g")]);

let from_five: Vec<_> = map.range(5..).collect();
assert_eq!(from_five, vec![(&5, &"e"), (&7, &"g")]);

let up_to_five: Vec<_> = map.range(..5).collect();
assert_eq!(up_to_five, vec![(&1, &"a"), (&3, &"c")]);

For an exclusive start (rare but it happens), reach for Bound::Excluded:

1
2
3
4
5
6
7
8
use std::collections::BTreeMap;
use std::ops::Bound::{Excluded, Unbounded};

let map: BTreeMap<i32, &str> =
    BTreeMap::from([(1, "a"), (3, "c"), (5, "e"), (7, "g")]);

let after_three: Vec<_> = map.range((Excluded(3), Unbounded)).collect();
assert_eq!(after_three, vec![(&5, &"e"), (&7, &"g")]);

Mutate in place with range_mut

range_mut gives you &mut V for each entry in the window — keys stay immutable (they have to, the map’s invariant depends on them):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::collections::BTreeMap;

let mut scores: BTreeMap<i32, i32> =
    BTreeMap::from([(1, 10), (3, 10), (5, 10), (7, 10)]);

for (_, v) in scores.range_mut(3..=5) {
    *v += 1;
}

assert_eq!(scores[&1], 10);
assert_eq!(scores[&3], 11);
assert_eq!(scores[&5], 11);
assert_eq!(scores[&7], 10);

The same trick works on BTreeSet::range.

Takeaway

When you find yourself filtering a BTreeMap by a range of keys, swap the filter for range. You get O(log n) seek plus O(k) iteration over just the matches, and the code reads more like the intent.

#176 Jun 2026

176. Option::as_deref — Stop Writing .as_ref().map(|s| s.as_str())

You have an Option<String>. The function next door takes Option<&str>. The chain you keep typing — .as_ref().map(|s| s.as_str()) — has a one-method replacement.

The pattern that keeps showing up

Borrowing the inside of an Option<String> looks like this:

1
2
3
4
let owned: Option<String> = Some("hello".to_string());

// Compare against a literal.
assert_eq!(owned.as_ref().map(|s| s.as_str()), Some("hello"));

as_ref() turns Option<String> into Option<&String>, then map reaches inside to pull out &str. Two methods, one closure, all just to borrow.

Option::as_deref collapses the whole thing:

1
2
let owned: Option<String> = Some("hello".to_string());
assert_eq!(owned.as_deref(), Some("hello"));

Same result, one call, no closure.

Why it works

as_deref is defined for any Option<T> where T: Deref. It calls Deref::deref on the inner value, so you get Option<&T::Target>:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let s: Option<String> = Some("hi".to_string());
let v: Option<Vec<i32>> = Some(vec![1, 2, 3]);
let b: Option<Box<i32>> = Some(Box::new(7));

let s_ref: Option<&str> = s.as_deref();      // String -> str
let v_ref: Option<&[i32]> = v.as_deref();    // Vec<T> -> [T]
let b_ref: Option<&i32> = b.as_deref();      // Box<T> -> T

assert_eq!(s_ref, Some("hi"));
assert_eq!(v_ref, Some(&[1, 2, 3][..]));
assert_eq!(b_ref, Some(&7));

Anything that derefs works, including your own types — implement Deref and as_deref follows for free.

Where it actually saves you

The most common use is feeding a borrowed view into a function that wants the unsized form:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn greet(name: Option<&str>) -> String {
    match name {
        Some(n) => format!("hi {n}"),
        None => "hi stranger".to_string(),
    }
}

let stored: Option<String> = Some("alice".to_string());
assert_eq!(greet(stored.as_deref()), "hi alice");

let missing: Option<String> = None;
assert_eq!(greet(missing.as_deref()), "hi stranger");

The stored value stays untouched — as_deref only borrows.

There’s also as_deref_mut for the &mut version, and Result::as_deref / as_deref_mut for the same trick on Result:

1
2
3
let r: Result<String, ()> = Ok("ok".to_string());
let borrowed: Result<&str, &()> = r.as_deref();
assert_eq!(borrowed, Ok("ok"));

Takeaway

Whenever you catch yourself typing .as_ref().map(|x| x.as_str()) or .as_ref().map(|x| &**x), reach for .as_deref(). One method, no closure, and it works on anything that derefs.

#175 Jun 2026

175. PathBuf::push — When an Absolute Argument Wipes Your Base Path

base.push(user_input) looks like string concatenation for paths. It isn’t — if user_input is absolute, the original base is gone.

The footgun

PathBuf::push reads almost like += for paths. Most of the time, it behaves that way:

1
2
3
4
5
6
use std::path::PathBuf;

let mut p = PathBuf::from("/home/alice");
p.push("docs");
p.push("notes.txt");
assert_eq!(p, PathBuf::from("/home/alice/docs/notes.txt"));

But the moment the pushed component is absolute, push throws the existing buffer away and starts over:

1
2
3
4
5
use std::path::PathBuf;

let mut p = PathBuf::from("/home/alice");
p.push("/etc/passwd");
assert_eq!(p, PathBuf::from("/etc/passwd"));

That’s not a bug. The docs spell it out: “if path is absolute, it replaces the current path.” It mirrors how cd /etc/passwd works in a shell. The catch is that when one half of push is user input, the cd-like behavior turns into a path-traversal vector.

Why this bites

The most common shape of the bug:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::path::PathBuf;

fn user_file(home: &str, requested: &str) -> PathBuf {
    let mut p = PathBuf::from(home);
    p.push(requested);
    p
}

assert_eq!(
    user_file("/srv/users/alice", "avatar.png"),
    PathBuf::from("/srv/users/alice/avatar.png"),
);

// An absolute `requested` silently escapes the sandbox.
assert_eq!(
    user_file("/srv/users/alice", "/etc/passwd"),
    PathBuf::from("/etc/passwd"),
);

No panic, no error, no warning. The function just hands back a path that points somewhere else entirely.

The fix

Reject absolute components before joining. Path::is_absolute and Path::has_root are the two checks you need:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::path::{Path, PathBuf};

fn safe_join(base: &Path, requested: &str) -> Option<PathBuf> {
    let segment = Path::new(requested);
    if segment.is_absolute() || segment.has_root() {
        return None;
    }
    Some(base.join(segment))
}

assert_eq!(
    safe_join(Path::new("/srv/users/alice"), "avatar.png"),
    Some(PathBuf::from("/srv/users/alice/avatar.png")),
);
assert_eq!(safe_join(Path::new("/srv/users/alice"), "/etc/passwd"), None);

has_root matters on Windows too — \windows\system32 has a root but no drive prefix, and push will replace the non-prefix portion of your buffer with it. is_absolute alone misses that case on Windows.

For full sandbox enforcement you also want to canonicalize and check the result is still under base.. components can still escape — but stopping the absolute-path case is the cheap first line of defence.

Takeaway

PathBuf::push is not string concatenation. Treat any component you didn’t write yourself as suspect and gate it through is_absolute / has_root before letting it near your buffer.

#174 May 2026

174. iter::repeat_with — Build N Fresh Things When vec![] Won't Clone

vec![Mutex::new(0); 10] — won’t compile. Mutex isn’t Clone. iter::repeat_with(|| Mutex::new(0)).take(10).collect() builds ten fresh ones instead.

The vec![x; n] trap

The vec![x; n] macro is the obvious way to build a Vec of n copies. It works by cloning x n times:

1
2
let zeros: Vec<u32> = vec![0; 5];
assert_eq!(zeros, vec![0, 0, 0, 0, 0]);

That’s fine for u32. It falls apart the moment you want something that isn’t Clone, or something where cloning gives you the wrong behavior — a counter, a channel endpoint, an Arc<Mutex<...>> you wanted to be separate locks:

1
2
3
use std::sync::Mutex;
// error[E0277]: the trait `Clone` is not implemented for `Mutex<u32>`
// let locks = vec![Mutex::new(0); 10];

The fix: iter::repeat_with

iter::repeat_with(f) takes a closure and yields f() every time the iterator is polled. Combine with take(n).collect() and you’ve got a builder that calls the closure exactly n times — no Clone required:

1
2
3
4
5
use std::iter;
use std::sync::Mutex;

let locks: Vec<Mutex<u32>> = iter::repeat_with(|| Mutex::new(0)).take(10).collect();
assert_eq!(locks.len(), 10);

Each Mutex is freshly constructed. They’re independent locks, not ten handles to the same one.

Stateful closures work too

Because the closure is FnMut, it can capture and mutate state — handy for counters, RNG-like generators, or anything where each element depends on the previous call:

1
2
3
4
5
use std::iter;

let mut n = 0;
let counts: Vec<u32> = iter::repeat_with(|| { n += 1; n }).take(5).collect();
assert_eq!(counts, vec![1, 2, 3, 4, 5]);

Try that with vec![...; 5] and you’d get five copies of the same number.

The function-pointer form

When the closure is just a constructor call, you can pass the function directly — no || needed:

1
2
3
4
5
use std::iter;

let buffers: Vec<Vec<i32>> = iter::repeat_with(Vec::new).take(3).collect();
assert_eq!(buffers.len(), 3);
assert!(buffers.iter().all(|b| b.is_empty()));

Vec::new here is a zero-argument function pointer, exactly what repeat_with wants.

When to reach for it

Any time you’d write a for loop to push n fresh items into a Vec, or any time vec![x; n] rejects you because the element isn’t Clone. It’s also the idiomatic way to seed parallel structures: repeat_with(|| Arc::new(Mutex::new(...))).take(workers).collect() gives each worker its own lock.

Available since Rust 1.28, lives in core::iter, no allocation overhead beyond the Vec itself.