#053 Mar 2026

53. element_offset — Find an Element's Index by Reference

Ever had a reference to an element inside a slice but needed its index? Before Rust 1.94, you’d reach for .position() with value equality or resort to pointer math. Now there’s a cleaner way.

The problem

Imagine you’re scanning a slice and a helper function hands you back a reference to the element it found. You know the reference points somewhere inside your slice, but you need the index — not a value-based search.

1
2
3
fn first_long_word<'a>(words: &'a [&str]) -> Option<&'a &'a str> {
    words.iter().find(|w| w.len() > 5)
}

You could call .position() with value comparison, but that re-scans the slice and compares by value — which is wasteful when you already hold the exact reference.

The solution: element_offset

<[T]>::element_offset takes a reference to an element and returns its Option<usize> index by comparing pointers, not values. If the reference points into the slice, you get Some(index). If it doesn’t, you get None.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn main() {
    let words = ["hi", "hello", "rustacean", "world"];

    // A helper hands us a reference into the slice
    let found: &&str = words.iter().find(|w| w.len() > 5).unwrap();

    // Get the index by reference identity — no value scan needed
    let index = words.element_offset(found).unwrap();

    assert_eq!(index, 2);
    assert_eq!(words[index], "rustacean");

    println!("Found '{}' at index {}", found, index);
}

Why not .position()?

.position() compares by value and has to walk the slice from the start. element_offset is an O(1) pointer comparison — it checks whether your reference falls within the slice’s memory range and computes the offset directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn main() {
    let values = [10, 20, 10, 30];

    let third = &values[2]; // points at the second '10'

    // position() finds the FIRST 10 (index 0) — wrong!
    let by_value = values.iter().position(|v| v == third);
    assert_eq!(by_value, Some(0));

    // element_offset() finds THIS exact element (index 2) — correct!
    let by_ref = values.element_offset(third);
    assert_eq!(by_ref, Some(2));

    println!("By value: {:?}, By reference: {:?}", by_value, by_ref);
}

This distinction matters whenever your slice has duplicate values.

When the reference is outside the slice

If the reference doesn’t point into the slice, you get None:

1
2
3
4
5
6
7
8
fn main() {
    let a = [1, 2, 3];
    let outside = &42;

    assert_eq!(a.element_offset(outside), None);

    println!("Outside reference: {:?}", a.element_offset(outside));
}

Clean, safe, and no unsafe pointer arithmetic required. Available since Rust 1.94.0.

52. Peekable::next_if_map — Peek, Match, and Transform in One Step

Tired of peeking at the next element, checking if it matches, and then consuming and transforming it? next_if_map collapses that entire dance into a single call.

The problem

When writing parsers or processing token streams, you often need to conditionally consume the next element and extract something from it. With next_if and peek, you end up doing the work twice — once to check, once to transform:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clunky: peek, check, consume, transform — separately
let val = if iter.peek().is_some_and(|n| *n > 0) {
    iter.next().map(|n| n * 10)
} else {
    None
};

assert_eq!(val, Some(10));

It works, but you’re expressing the same logic in two places and the code doesn’t clearly convey its intent.

Enter next_if_map

Stabilized in Rust 1.94, Peekable::next_if_map takes a closure that returns Result<R, I::Item>. Return Ok(transformed) to consume the element, or Err(original) to put it back:

1
2
3
4
5
6
7
8
9
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clean: one closure handles the check AND the transformation
let val = iter.next_if_map(|n| {
    if n > 0 { Ok(n * 10) } else { Err(n) }
});

assert_eq!(val, Some(10));
assert_eq!(iter.next(), Some(2)); // iterator continues normally

The element is only consumed when you return Ok. If you return Err, the original value goes back and the iterator is unchanged.

Parsing example: extract leading digits

This is where next_if_map really shines — pulling typed tokens out of a character stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut chars = "42abc".chars().peekable();

let mut number = 0u32;
while let Some(digit) = chars.next_if_map(|c| {
    c.to_digit(10).ok_or(c)
}) {
    number = number * 10 + digit;
}

assert_eq!(number, 42);
assert_eq!(chars.next(), Some('a')); // non-digit stays unconsumed

Each character is inspected once: digits are consumed and converted, and the first non-digit stops the loop without being eaten.

Key details

  • Atomic peek + consume + transform: no redundant checks, no repeated logic
  • Non-destructive on rejection: returning Err(item) puts the element back
  • Also available: next_if_map_mut takes FnOnce(&mut I::Item) -> Option<R> for when you don’t need ownership
  • Stable since Rust 1.94

Next time you’re writing a peek-then-consume pattern, reach for next_if_map — your parser will thank you.

51. File::lock — File Locking in the Standard Library

Multiple processes writing to the same file? That’s a recipe for corruption. Since Rust 1.89, File::lock gives you OS-backed file locking without external crates.

The problem

You have a CLI tool that appends to a shared log file. Two instances run at the same time, and suddenly your log entries are garbled — half a line from one process interleaved with another. Before 1.89, you’d reach for the fslock or file-lock crate. Now it’s built in.

Exclusive locking

File::lock() acquires an exclusive (write) lock. Only one handle can hold an exclusive lock at a time — all other attempts block until the lock is released:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::fs::File;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    let mut file = File::options()
        .write(true)
        .create(true)
        .open("/tmp/rustbites_lock_demo.txt")?;

    // Blocks until the lock is acquired
    file.lock()?;

    writeln!(file, "safe write from process {}", std::process::id())?;

    // Lock is released when the file is closed (dropped)
    Ok(())
}

When the File is dropped, the lock is automatically released. No manual unlock() needed — though you can call file.unlock() explicitly if you want to release it early.

Shared (read) locking

Sometimes you want to allow multiple readers but block writers. That’s what lock_shared() is for:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::fs::File;
use std::io::{self, Read};

fn main() -> io::Result<()> {
    let mut file = File::open("/tmp/rustbites_lock_demo.txt")?;

    // Multiple processes can hold a shared lock simultaneously
    file.lock_shared()?;

    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    println!("Read: {contents}");

    file.unlock()?; // explicit release
    Ok(())
}

Shared locks coexist with other shared locks, but block exclusive lock attempts. Classic reader-writer pattern, enforced at the OS level.

Non-blocking with try_lock

Don’t want to wait? try_lock() and try_lock_shared() return immediately instead of blocking:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::fs::{self, File, TryLockError};

fn main() -> std::io::Result<()> {
    let file = File::options()
        .write(true)
        .create(true)
        .open("/tmp/rustbites_trylock.txt")?;

    match file.try_lock() {
        Ok(()) => println!("Lock acquired!"),
        Err(TryLockError::WouldBlock) => println!("File is busy, try later"),
        Err(TryLockError::Error(e)) => return Err(e),
    }

    Ok(())
}

If another process holds the lock, you get TryLockError::WouldBlock instead of hanging. Perfect for tools that should fail fast rather than block when another instance is already running.

Key details

  • Advisory locks: these locks are advisory on most platforms — they don’t prevent other processes from reading/writing the file unless those processes also use locking
  • Automatic release: locks are released when the File handle is dropped
  • Cross-platform: works on Linux, macOS, and Windows (uses flock on Unix, LockFileEx on Windows)
  • Stable since Rust 1.89

50. slice::chunk_by — Group Consecutive Elements

Need to split a slice into groups of consecutive elements that share a property? chunk_by does exactly that — no allocations, no manual index tracking.

The problem

Imagine you have a sorted list of temperatures and want to group them into runs of non-decreasing values. Without chunk_by, you’d write a loop tracking where each group starts and ends:

1
2
let temps = [18, 20, 22, 19, 21, 25, 24];
// Manual grouping... indices, slicing, off-by-one bugs 😬

Enter chunk_by

Stabilized in Rust 1.77, slice::chunk_by splits a slice between consecutive elements where the predicate returns false. Each chunk is a sub-slice where every adjacent pair satisfies the predicate:

1
2
3
4
5
6
7
8
9
let temps = [18, 20, 22, 19, 21, 25, 24];

let runs: Vec<&[i32]> = temps.chunk_by(|a, b| a <= b).collect();

assert_eq!(runs, vec![
    &[18, 20, 22] as &[i32],
    &[19, 21, 25],
    &[24],
]);

The predicate |a, b| a <= b keeps elements in the same chunk as long as values are non-decreasing. The moment a value drops, a new chunk begins.

Group by equality

A common use case is grouping runs of equal elements:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let data = [1, 1, 2, 3, 3, 3, 2, 2];

let groups: Vec<&[i32]> = data.chunk_by(|a, b| a == b).collect();

assert_eq!(groups, vec![
    &[1, 1] as &[i32],
    &[2],
    &[3, 3, 3],
    &[2, 2],
]);

Notice this groups consecutive equal elements — it’s not the same as a GROUP BY in SQL. The two runs of 2 stay separate because they aren’t adjacent.

Mutable chunks

There’s also chunk_by_mut if you need to modify elements within each group:

1
2
3
4
5
6
7
8
let mut data = [1, 1, 2, 3, 3, 3, 2, 2];

for chunk in data.chunk_by_mut(|a, b| a == b) {
    // Double the first element in each run
    chunk[0] *= 2;
}

assert_eq!(data, [2, 1, 4, 6, 3, 3, 4, 2]);

Key details

  • Zero-cost: returns sub-slices of the original data — no allocations
  • Predicate sees pairs: |a, b| receives each consecutive pair; a new chunk starts where it returns false
  • Works on any slice: &[T], &mut [T], Vec<T> (via deref)
  • Stable since Rust 1.77

Next time you reach for a manual loop to group consecutive elements, try chunk_by instead.

#049 Mar 2026

49. std::io::pipe — Anonymous Pipes in the Standard Library

Need to wire up stdout and stderr from a child process, or stream data between threads? Since Rust 1.87, std::io::pipe() gives you OS-backed anonymous pipes without reaching for external crates.

What’s an anonymous pipe?

A pipe is a one-way data channel: one end writes, the other reads. Before 1.87, you needed the os_pipe crate or platform-specific code to get one. Now it’s a single function call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    let (mut reader, mut writer) = io::pipe()?;

    writer.write_all(b"hello from the pipe")?;
    drop(writer); // close the write end so reads hit EOF

    let mut buf = String::new();
    reader.read_to_string(&mut buf)?;
    assert_eq!(buf, "hello from the pipe");

    println!("Received: {buf}");
    Ok(())
}

pipe() returns a (PipeReader, PipeWriter) pair. PipeReader implements Read, PipeWriter implements Write — they plug into any generic I/O code you already have.

Merge stdout and stderr from a child process

The killer use case: capture both output streams from a subprocess as a single interleaved stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::io::{self, Read};
use std::process::Command;

fn main() -> io::Result<()> {
    let (mut recv, send) = io::pipe()?;

    let mut child = Command::new("echo")
        .arg("hello world")
        .stdout(send.try_clone()?)
        .stderr(send)
        .spawn()?;

    child.wait()?;

    let mut output = String::new();
    recv.read_to_string(&mut output)?;
    assert!(output.contains("hello world"));

    println!("Combined output: {output}");
    Ok(())
}

The try_clone() on the writer lets both stdout and stderr write to the same pipe. When both copies of the PipeWriter are dropped (one moved into stdout, one into stderr), reads on the PipeReader return EOF.

Why not just use Command::output()?

Command::output() captures stdout and stderr separately into Vec<u8> — you get two blobs, no interleaving, and everything is buffered in memory. With pipes, you can stream the output as it arrives, merge the two streams, or fan data into multiple consumers. Pipes give you the plumbing; output() gives you the convenience.

Key behavior

A read on PipeReader blocks until data is available or all writers are closed. A write on PipeWriter blocks when the OS pipe buffer is full. This is the same behavior as Unix pipes under the hood — because that’s exactly what they are.

#048 Mar 2026

48. #[expect] — Lint Suppression That Cleans Up After Itself

Silencing a lint with #[allow] is easy — but forgetting to remove it when the code changes is even easier. #[expect] suppresses a lint and warns you when it’s no longer needed.

The problem with #[allow]

You add #[allow(unused_variables)] during a refactor, the code ships, months pass, the variable gets used — and the stale #[allow] stays forever:

1
2
3
4
5
#[allow(unused_variables)]
let connection = db.connect(); // was unused during refactor
// ... later someone adds:
connection.execute("SELECT 1");
// the #[allow] above is now pointless, but no one notices

Over time, your codebase collects these like dust. They suppress diagnostics for problems that no longer exist.

Enter #[expect]

Stabilized in Rust 1.81, #[expect] works exactly like #[allow] — but fires a warning when the lint it suppresses is never triggered:

1
2
3
4
5
6
#[expect(unused_variables)]
let unused = "Suppressed — and the compiler knows why";

#[expect(unused_variables)] // ⚠️ warning: this expectation is unfulfilled
let used = "I'm actually used!";
println!("{used}");

The first #[expect] is satisfied — the variable is unused, and the lint stays quiet. The second one triggers unfulfilled_lint_expectations because used isn’t actually unused. The compiler tells you: this suppression has no reason to exist.

Add a reason for future you

#[expect] supports an optional reason parameter that shows up in the warning message:

1
2
3
4
#[expect(dead_code, reason = "will be used once the API module lands")]
fn prepare_response() -> Vec<u8> {
    vec![0x00, 0xFF]
}

When prepare_response gets called and the expectation becomes unfulfilled, the warning includes your reason — so future-you (or a teammate) knows exactly why it was there and that it’s safe to remove.

Works with Clippy too

#[expect] isn’t limited to compiler lints — it works with Clippy:

1
2
3
4
#[expect(clippy::needless_return)]
fn legacy_style() -> i32 {
    return 42;
}

This is perfect for migrating a codebase to stricter Clippy rules incrementally. Suppress violations with #[expect], fix them over time, and the compiler will tell you when each suppression can go.

#[allow] vs #[expect] — when to use which

Use #[allow] when the suppression is permanent and intentional — you never want the lint to fire here. Use #[expect] when the suppression is temporary or when you want a reminder to revisit it. Think of #[expect] as a // TODO that the compiler actually enforces.

#047 Mar 2026

47. Vec::pop_if — Conditionally Pop the Last Element

Need to remove the last element of a Vec only when it meets a condition? Vec::pop_if does exactly that — no index juggling, no separate check-then-pop.

The old way

Before pop_if, you’d write something like this:

1
2
3
4
5
6
let mut stack = vec![1, 2, 3, 4];

if stack.last().is_some_and(|x| *x > 3) {
    let val = stack.pop().unwrap();
    println!("Popped: {val}");
}

Two separate calls, and a subtle TOCTOU gap if you’re not careful — last() checks one thing, then pop() acts on an assumption.

Enter pop_if

Stabilized in Rust 1.86, pop_if combines the check and the removal into one atomic operation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut stack = vec![1, 2, 3, 4];

// Pop the last element only if it's greater than 3
let popped = stack.pop_if(|x| *x > 3);
assert_eq!(popped, Some(4));
assert_eq!(stack, [1, 2, 3]);

// Now the last element is 3 — doesn't match, so nothing happens
let stayed = stack.pop_if(|x| *x > 3);
assert_eq!(stayed, None);
assert_eq!(stack, [1, 2, 3]);

The closure receives a &mut T reference to the last element. If it returns true, the element is removed and returned as Some(T). If false (or the vec is empty), you get None.

Mutable access inside the predicate

Because the closure gets &mut T, you can even modify the element before deciding whether to pop it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let mut tasks = vec![
    String::from("buy milk"),
    String::from("URGENT: deploy fix"),
];

let urgent = tasks.pop_if(|task| {
    if task.starts_with("URGENT:") {
        *task = task.replacen("URGENT: ", "", 1);
        true
    } else {
        false
    }
});

assert_eq!(urgent.as_deref(), Some("deploy fix"));
assert_eq!(tasks, vec!["buy milk"]);

A practical use: draining from the back

pop_if is handy for processing a sorted vec from the tail. Think of a priority queue backed by a sorted vec where you only want to process items above a threshold:

1
2
3
4
5
6
7
8
9
let mut scores = vec![10, 25, 50, 75, 90];

let mut high_scores = Vec::new();
while let Some(score) = scores.pop_if(|s| *s >= 70) {
    high_scores.push(score);
}

assert_eq!(high_scores, vec![90, 75]);
assert_eq!(scores, vec![10, 25, 50]);

Clean, expressive, and no off-by-one errors. Another small addition to Vec that makes everyday Rust just a bit nicer.

46. Let Chains — Flatten Nested if-let with &&

Deeply nested if let blocks are one of Rust’s most familiar awkward moments. Rust 1.88 (2024 edition) finally fixes it: let chains let you &&-chain multiple let bindings and boolean guards in one if or while.

Before let chains, matching several optional values forced you to nest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn describe(name: Option<&str>, age: Option<u32>) -> String {
    if let Some(n) = name {
        if let Some(a) = age {
            if a >= 18 {
                return format!("{n} is an adult");
            }
        }
    }
    "unknown".to_string()
}

Three levels of indentation just to check two Options and a condition. The actual logic is buried at the bottom.

With let chains, it collapses to a single if:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn describe(name: Option<&str>, age: Option<u32>) -> String {
    if let Some(n) = name
        && let Some(a) = age
        && a >= 18
    {
        format!("{n} is an adult")
    } else {
        "unknown".to_string()
    }
}

Bindings introduced in earlier lets are in scope for all subsequent conditions — n is available when checking a, and both are available in the body.

The same syntax works in while loops. This example processes tokens until it hits one it can’t parse:

1
2
3
4
5
6
7
8
9
let mut tokens = vec!["42", "17", "bad", "99"];
tokens.reverse(); // process front-to-back

while let Some(token) = tokens.pop()
    && let Ok(n) = token.parse::<i32>()
{
    println!("{n}");
}
// prints: 42, 17  — stops at "bad"

Short-circuit semantics apply: if any condition in the chain fails, Rust skips the rest and takes the else branch (or ends the while loop).

To enable let chains, set edition = "2024" in your Cargo.toml:

1
2
[package]
edition = "2024"

No unstable flags, no feature gates — it’s stable as of Rust 1.88 and available on the 2024 edition. If you’re still on 2021, this alone is a good reason to upgrade.

45. get_disjoint_mut — Multiple Mutable References at Once

The borrow checker won’t let you hold two &mut refs into the same collection — even when you know they don’t overlap. get_disjoint_mut fixes that without unsafe.

The problem

You want to update two elements of the same Vec together, but the compiler won’t allow two mutable borrows at once:

1
2
3
4
let mut scores = vec![10u32, 20, 30, 40];
let a = &mut scores[0];
let b = &mut scores[2]; // ❌ cannot borrow `scores` as mutable more than once
*a += *b;

The borrow checker doesn’t know indices 0 and 2 are different slots — it just sees two &mut to the same Vec. The classic escape hatches (split_at_mut, unsafe, RefCell) all feel like workarounds for something that should just work.

get_disjoint_mut to the rescue

Stabilized in Rust 1.86, get_disjoint_mut accepts an array of indices and returns multiple mutable references — verified at runtime to be non-overlapping:

1
2
3
4
5
6
7
let mut scores = vec![10u32, 20, 30, 40];

if let Ok([a, b]) = scores.get_disjoint_mut([0, 2]) {
    *a += *b; // 10 + 30 = 40
}

assert_eq!(scores, [40, 20, 30, 40]); // ✅

The Result is Err only if an index is out of bounds or indices overlap. Duplicate indices are caught at runtime and return Err — no silent aliasing bugs.

Works on HashMap as well

HashMap gets the same treatment. The return type is [Option<&mut V>; N] — one Option per key, since keys can be missing:

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

let mut accounts: HashMap<&str, u32> = HashMap::from([
    ("alice", 100),
    ("bob", 200),
]);

// Transfer 50 from bob to alice
let [alice, bob] = accounts.get_disjoint_mut(["alice", "bob"]);
if let (Some(a), Some(b)) = (alice, bob) {
    *a += 50;
    *b -= 50;
}

assert_eq!(accounts["alice"], 150);
assert_eq!(accounts["bob"], 150); // ✅

Passing duplicate keys to the HashMap version panics — the right tradeoff for a bug that would otherwise silently produce undefined behavior.

When to reach for it

  • Swapping or combining two elements in a Vec without split_at_mut gymnastics
  • Updating multiple HashMap entries in one pass
  • Any place you’d have used unsafe or RefCell just to hold two &mut into the same container

If your indices or keys are known not to overlap, get_disjoint_mut is the clean, safe answer.

#044 Mar 2026

44. split_once — Split a String Exactly Once

When you need to split a string on the first occurrence of a delimiter, split_once is cleaner than anything you’d write by hand. Stable since Rust 1.52.

Parsing key=value pairs, HTTP headers, file paths — almost everywhere you split a string, you only care about the first separator. Before split_once, you’d reach for .find() plus index arithmetic:

The old way

1
2
3
4
5
6
7
8
let s = "Content-Type: application/json; charset=utf-8";

let colon = s.find(':').unwrap();
let header = &s[..colon];
let value = s[colon + 1..].trim();

assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");

Works, but it’s four lines of noise. The index arithmetic is easy to get wrong, and .trim() is a separate step.

With split_once

1
2
3
4
5
6
let s = "Content-Type: application/json; charset=utf-8";

let (header, value) = s.split_once(": ").unwrap();

assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");

One line. The delimiter is consumed, both sides are returned, and you pattern-match directly into named bindings.

Handling missing delimiters

split_once returns Option<(&str, &str)>None if the delimiter isn’t found. This makes it composable with ? or if let:

1
2
3
4
5
6
7
fn parse_env_var(s: &str) -> Option<(&str, &str)> {
    s.split_once('=')
}

assert_eq!(parse_env_var("HOME=/root"), Some(("HOME", "/root")));
assert_eq!(parse_env_var("NOVALUE"), None);
assert_eq!(parse_env_var("KEY=a=b=c"), Some(("KEY", "a=b=c")));

Note the last case: split_once stops at the first =. The rest of the string — a=b=c — is kept intact in the second half. That’s usually exactly what you want.

rsplit_once — split from the right

When you need the last delimiter instead of the first, rsplit_once has you covered:

1
2
3
4
5
6
let path = "/home/martin/projects/rustbites/content/posts/bite-044.md";

let (dir, filename) = path.rsplit_once('/').unwrap();

assert_eq!(dir, "/home/martin/projects/rustbites/content/posts");
assert_eq!(filename, "bite-044.md");

Multi-char delimiters work too

The delimiter can be any pattern — a char, a &str, or even a closure:

1
2
3
4
5
6
7
8
let record = "alice::42::engineer";

let (name, rest) = record.split_once("::").unwrap();
let (age_str, role) = rest.split_once("::").unwrap();

assert_eq!(name, "alice");
assert_eq!(age_str, "42");
assert_eq!(role, "engineer");

Whenever you reach for .splitn(2, ...) just to grab two halves, replace it with split_once — the intent is clearer and the return type is more ergonomic.