api: add Cow guarantee to replace API#1178
Merged
BurntSushi merged 1 commit intomasterfrom Mar 23, 2024
Merged
Conversation
This adds a guarantee to the API of the `replace`, `replace_all` and `replacen` routines that, when `Cow::Borrowed` is returned, it is guaranteed that it is equivalent to the `haystack` given. The implementation has always matched this behavior, but this elevates the implementation behavior to an API guarantee. There do exists implementations where this guarantee might not be upheld in every case. For example, if the final result were the empty string, we could return a `Cow::Borrowed`. Similarly, if the final result were a substring of `haystack`, then `Cow::Borrowed` could be returned in that case too. In practice, these sorts of optimizations are tricky to do in practice, and seem like niche corner cases that aren't important to optimize. Nevertheless, having this guarantee is useful because it can be used as a signal that the original input remains unchanged. This came up in discussions with @quicknir on Discord. Namely, in cases where one is doing a sequence of replacements and in most cases nothing is replaced, using a `Cow` is nice to be able to avoid copying the haystack over and over again. But to get this to work right, you have to know whether a `Cow::Borrowed` matches the input or not. If it doesn't, then you'd need to transform it into an owned string. For example, this code tries to do replacements on each of a sequence of `Cow<str>` values, where the common case is no replacement: ```rust use std::borrow::Cow; use regex::Regex; fn trim_strs(strs: &mut Vec<Cow<str>>) { strs .iter_mut() .for_each(|s| moo(s, ®ex_replace)); } fn moo<F: FnOnce(&str) -> Cow<str>>(c: &mut Cow<str>, f: F) { let result = f(&c); match result { Cow::Owned(s) => *c = Cow::Owned(s), Cow::Borrowed(s) => { *c = Cow::Borrowed(s); } } } fn regex_replace(s: &str) -> Cow<str> { Regex::new(r"does-not-matter").unwrap().replace_all(s, "whatever") } ``` But this doesn't pass `borrowck`. Instead, you could write `moo` like this: ```rust fn moo<F: FnOnce(&str) -> Cow<str>>(c: &mut Cow<str>, f: F) { let result = f(&c); match result { Cow::Owned(s) => *c = Cow::Owned(s), Cow::Borrowed(s) => { if !std::ptr::eq(s, &**c) { *c = Cow::Owned(s.to_owned()) } } } } ``` But the `std::ptr:eq` call here is a bit strange. Instead, after this PR and the new guarantee, one can write it like this: ```rust fn moo<F: FnOnce(&str) -> Cow<str>>(c: &mut Cow<str>, f: F) { if let Cow::Owned(s) = f(&c) { *c = Cow::Owned(s); } } ```
|
Looks great! Both the change and the write up. Thanks very much, very appreciated! |
Member
Author
|
This PR is on crates.io in |
|
I'm the person that initially ran into this case and talked to quicknir about it. Thank you both very much for your help and efforts! |
This was referenced Oct 13, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds a guarantee to the API of the
replace,replace_allandreplacenroutines that, whenCow::Borrowedis returned, it isguaranteed that it is equivalent to the
haystackgiven.The implementation has always matched this behavior, but this elevates
the implementation behavior to an API guarantee.
There do exists implementations where this guarantee might not be upheld
in every case. For example, if the final result were the empty string,
we could return a
Cow::Borrowed. Similarly, if the final result were asubstring of
haystack, thenCow::Borrowedcould be returned in thatcase too. In practice, these sorts of optimizations are tricky to do in
practice, and seem like niche corner cases that aren't important to
optimize.
Nevertheless, having this guarantee is useful because it can be used as
a signal that the original input remains unchanged. This came up in
discussions with @quicknir on Discord. Namely, in cases where one is
doing a sequence of replacements and in most cases nothing is replaced,
using a
Cowis nice to be able to avoid copying the haystack over andover again. But to get this to work right, you have to know whether a
Cow::Borrowedmatches the input or not. If it doesn't, then you'd needto transform it into an owned string. For example, this code tries to do
replacements on each of a sequence of
Cow<str>values, where thecommon case is no replacement:
But this doesn't pass
borrowck. Instead, you could writemoolikethis:
But the
std::ptr:eqcall here is a bit strange. Instead, after this PRand the new guarantee, one can write it like this: