We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 18 of 21
Cow in API design: impl Into<Cow<str>>
This one is about designing with Cow, not just returning one. Three pieces:
pub struct Message<'a> { text: Cow<'a, str> }
impl<'a> Message<'a> {
pub fn new(text: impl Into<Cow<'a, str>>) -> Self
}
pub fn render(template: &str, vars: Vec<(String, String)>) -> Cow<'_, str>
pub fn build(inputs: Vec<String>) -> Vec<(String, bool)>
render substitutes {name} placeholders from vars:
-
a
{name}whose key is invarsis replaced by its value; -
a
{name}whose key is absent is left verbatim, braces and all; -
an unclosed
{and everything after it is copied through as-is; -
if the template contains no
{at all, returnCow::Borrowed(template)and allocate nothing. Otherwise returnCow::Owned, even if nothing was actually substituted.
build exercises Message. For each input string s: if s is empty,
construct Message::new("(empty)") from a string literal; otherwise construct
Message::new(format!("<{s}>")) from an owned String. Return
(text, was_borrowed) for each. The literal ones must come back borrowed;
the formatted ones owned. That pair of assertions is the whole point of
the parameter type.
Error one: E0106, and the cascade behind it
pub struct Message {
text: Cow<str>,
}
error[E0106]: missing lifetime specifier
Cow<'a, B> has a lifetime parameter because its Borrowed variant holds a
reference, and a reference always needs to say how long the thing it points
at lives. There is no elision rule for struct fields — elision only works
where the compiler can copy a lifetime from somewhere obvious, and a struct
definition has no “somewhere obvious”.
So the fix is struct Message<'a> { text: Cow<'a, str> }, and that is the
cost you are buying into. The struct is now generic over a lifetime, which
means:
impl<'a> Message<'a> { ... }
fn handle(m: Message<'_>) { ... }
struct Inbox<'a> { messages: Vec<Message<'a>> } // and now Inbox is too
fn load<'a>(raw: &'a str) -> Inbox<'a> { ... } // and now every function is
The 'a propagates outward through every type that contains a Message and
every signature that mentions one. This is the honest reason people abandon
Cow halfway through a refactor: they add it to one leaf type and then spend
an afternoon threading a lifetime through forty signatures.
Sometimes String is just better. If the struct is long-lived, stored in
a collection, sent across a thread, or held in a global, it cannot borrow
from a short-lived buffer anyway, and Cow buys you nothing but ceremony.
Use Cow in a struct when the struct is a short-lived view over data
somebody else owns — a parsed header, a request being processed, a row being
formatted — and the borrowed case is the common one.
Error two: E0308, and why impl Into<Cow<'a, str>>
The starter’s constructor takes a String:
pub fn new(text: String) -> Self
error[E0308]: mismatched types
Message::new("(empty)")
^^^^^^^^^ expected `String`, found `&str`
You could “fix” that with .to_string() at the call site — and you would
have forced an allocation on a caller who already had a perfectly good
&'static str. The API would be quietly taxing every user who has the cheap
thing.
The fix is to accept whichever they have:
pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
Message { text: text.into() }
}
std provides From<&'a str> for Cow<'a, str> (producing Borrowed) and
From<String> for Cow<'a, str> (producing Owned). So:
Message::new("hello") // Borrowed, zero allocations
Message::new(format!("{x}")) // Owned, the allocation you already paid for
Message::new(some_cow) // passes straight through
One function, three call shapes, no conversion anywhere. This is how ergonomic Rust APIs avoid forcing allocations on their callers, and it is not something you arrive at by accident — you have to know the pattern exists.
::: question impl Into<Cow<'a, str>> versus the more familiar impl AsRef<str>. How do you choose?
The decision rule is one question: do I sometimes need to store it?
AsRef<str> says “I will look at your string and I promise not to keep it”.
It accepts &str, String, Cow, &String, Box<str> — a wider set — and
it is the right bound for a function that reads and returns something else.
fn word_count(s: impl AsRef<str>) -> usize should be AsRef.
Into<Cow<'a, str>> says “I may keep it, and I would like to avoid
allocating if you already own it or if it will outlive me”. It is the right
bound for constructors and for anything that stores its argument. If you took
AsRef<str> and then had to store it, you would be forced to call
.as_ref().to_owned() — allocating even when the caller handed you a
String they were about to throw away.
So: reading only, AsRef. Storing sometimes, Into<Cow>. Storing always,
just take String and let the caller decide.
:::
render: the borrowed fast path
The specification says “no { in the template means Cow::Borrowed“. That
is deliberately a syntactic test rather than a semantic one, and it is
worth noticing why: a check you can do in one cheap scan is a check you will
actually perform. “Return borrowed if nothing changed” would require doing
all the work first and then comparing, which costs more than it saves.
Real APIs make exactly this trade. str::trim returns a sub-slice, so it is
free. percent_encoding::utf8_percent_encode returns a Cow and borrows
when nothing needed encoding. Both pick a cheap conservative test over a
perfect one.
For the substitution itself, find('{') and slicing gets you there without
building an intermediate Vec<char>. Remember that find returns a byte
index, and that slicing a &str at a non-boundary panics — but { and }
are ASCII, so the boundaries are safe here.
Lints to expect
-
owned_cow(default-on) fires on a field declaredCow<'static, String>: “needlessly owned Cow type”. The inner type must be the borrowed form —str, notString. -
ptr_arg(default-on) fires on&Cow<'_, str>parameters: “using a reference toCowis not recommended”. Take theCowby value, or take a&str. -
suspicious_to_owned(default-on) catchescow.to_owned(), which clones the wrapper rather than the contents. You meantinto_owned().
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.