Three ToOwned katas, run¶
How to learn ToOwned › Beside the steps · read after: Step 6 — One blanket impl and Step 7 — Borrow is the way back
Level: 201 · a companion to the path
One line: A set of three ToOwned katas, each with tests and a solution, is circulating: to_owned on str and slices, a generic T: ?Sized + ToOwned function, and a custom ToOwned impl. All three compile and pass their own tests on rustc 1.98.0. The first two are sound, and the second has one wrong word. The third teaches the one thing std's ToOwned impls never do, a to_owned that changes the value, and Cow and HashMap both misbehave as a result.
The verdict¶
| Kata | Compiles, tests pass | Verdict | What to do |
|---|---|---|---|
1 — .to_owned() on str and slices |
yes | correct, and a warm-up | its first two functions are the ToOwned page's opening example; step 1's kata asks the same thing with predictions attached |
2 — a generic ToOwned bound over &[&T] |
yes | a good kata; "silently" is wrong where it counts | do it as step 6's kata, which keeps the trap and says where it is loud |
3 — a custom ToOwned that canonicalizes |
yes | compiles, and teaches a design that breaks Cow and map lookups |
do the repaired version below: check at the door, copy in to_owned |
The run¶
One program runs the claims from all three katas. Kata 3's design is rebuilt as the kata gives it: a view over any str, with a to_owned that canonicalizes.
Verified output of to_owned_katas_checked.rs — regenerated by tools/run_examples.py, never hand-typed.
Kata 1. "str: ToOwned<Owned = String>, [T]: ToOwned<Owned = Vec<T>>"
text.to_owned() -> alloc::string::String
nums.to_owned() -> alloc::vec::Vec<i32>
words -> ["a", "bb", "ccc"] holds.
Kata 2. "x.to_owned() on a &&T would silently return &T"
Vec<_> from .map(|x| x.to_owned()) -> alloc::vec::Vec<&str> silent here
inside clone_each, Vec<T::Owned> is asked for -> E0277, not silent
clone_each(&[&str]) -> alloc::vec::Vec<alloc::string::String>
clone_each(&[&Path]) -> alloc::vec::Vec<std::path::PathBuf>
clone_each(&[&i32]) -> alloc::vec::Vec<i32> (the blanket impl: Owned = T)
Kata 3. "a custom ToOwned whose Owned form is a transformed value"
the kata's own round trip starts from a canonical name:
tidy.to_owned().borrow() == tidy ? true
the same round trip from the input the kata is about:
raw.to_owned().borrow() == raw ? false
Cow: to_mut() is documented to CLONE; here it rewrites the value
before to_mut: " Grace Hopper " after: "grace-hopper"
Cow::Borrowed(raw) == Cow::Owned(raw.to_owned()) ? false
HashMap<Username, _>: a lookup by the borrowed form, as Borrow promises
insert UsernameRef::new("Ada").to_owned(), then:
get(UsernameRef::new("Ada")) = None
get(UsernameRef::new("ada")) = Some(3)
Kata 1: .to_owned() on str and slices¶
Every claim holds. str's impl gives a String, [T]'s gives a Vec<T> given T: Clone, and split_whitespace().map(str::to_owned) collects the words as owned Strings. The run prints them under Kata 1.
Two notes, neither a mistake. The rule against to_string() is about style, not speed: the two have cost the same since Rust 1.9, and the argument for to_owned() that survives is that it names what changes, the owner. And the untouched stub prints three unused variable warnings, one per todo!() body, which go away as you fill them in.
Kata 2: a generic ToOwned bound¶
This is a good exercise. clone_each<T: ?Sized + ToOwned>(xs: &[&T]) -> Vec<T::Owned> is step 1's signature one level deeper, and the solution's comment gives the right reason for the (*x): x is a &&T, and the dot takes the first receiver that fits. That is the blanket impl for &T, whose Owned is &T.
One word is wrong: "silently". In the kata's own function, x.to_owned() does not compile. The return type asks for T::Owned, and rustc 1.98.0 says "a value of type Vec<<T as std::borrow::ToOwned>::Owned> cannot be built from an iterator over elements of type &T" (E0277), with a note pointing at the map where Iterator::Item changed from &&T to &T. It is silent only where nothing asks for T::Owned: collect into a Vec<_> and you get a Vec<&str> without a word, as the run prints under Kata 2. No lint catches that case either (what no lint catches). The concrete version, over a &[&str], is error 9 on Every ToOwned error.
Smaller things, all measured:
?Sizedallows an unsizedT; it does not require one. The doc comment listsstr,[i32]andPath, butclone_each(&[&1, &2])works too, and gives aVec<i32>through the blanket impl..map(T::to_owned)without.copied()isE0631, "type mismatch in function arguments":maphands the function a&&T, and a function path gets no deref coercion..copied().map(T::to_owned)and.map(|&x| x.to_owned())both work, and step 6's kata runs them.- The name says clone, but for
T = strnothing is cloned:stris notClone, and its hand-written impl builds aString.own_eachwould be more accurate. - The stub's imports warn outside the tests.
use std::path::{Path, PathBuf}is used only inmod tests, so a plain build warns "unused imports:PathBufandPath".use std::borrow::ToOwnedis already in the prelude, which only the allow-by-defaultredundant_importslint reports.
Kata 3: a ToOwned that changes the value¶
The kata builds a #[repr(transparent)] UsernameRef(str) that any &str can become, and asks for a to_owned that lowercases, trims and joins words with -. It compiles, its four tests pass, and it passes clippy's default lints. The shape is right, since it is how std builds Path over OsStr and how Implementing ToOwned builds AsciiStr. The job it gives to_owned is wrong.
Std's docs describe to_owned as creating owned data from borrowed data, "usually by cloning", and every impl std has is a copy. str copies its bytes into a String, [T] calls to_vec, Path calls to_path_buf, and OsStr calls to_os_string. Cow relies on that. Its to_mut and into_owned are each documented as "Clones the data if it is not already owned", and they do it by calling to_owned. Give to_owned a different job and three things go wrong. The run shows them under Kata 3:
- The round trip only holds for input that is already canonical.
type Owned: Borrow<Self>exists so you can go to the owned form and borrow your way back. From" Grace Hopper "you come back to"grace-hopper", a different value. The kata's ownborrow_roundtriptest cannot see this, because it starts from aUsername, which is always canonical. Cow::to_mut()rewrites the text. ACowis supposed to borrow until somebody writes. Here, asking for write access, before writing anything, turns" Grace Hopper "into"grace-hopper". The same input asCow::Borrowedand asCow::Ownedcompares unequal.- The map misses a key that was inserted. Insert
UsernameRef::new("Ada").to_owned()into aHashMap<Username, _>, then look it up withUsernameRef::new("Ada"), the same text you started from. The result isNone. Only"ada"finds it.
Borrow's written promise is kept. Two Usernames compare equal exactly when their borrowed views do, so this is not the CaseInsensitive trap, where the impl itself lies. What breaks is the assumption underneath: a &UsernameRef should be something a Username could lend. UsernameRef::new hands out views that no Username ever lends, and every generic API that goes through to_owned turns them into a different value.
The fix moves the canonicalizing to the way in. Username::new(raw) canonicalizes. UsernameRef::parse(s) accepts only text that is already canonical. to_owned goes back to being a copy. This is the "check at the only way in" pattern from Implementing ToOwned, and the practice below builds it.
Three smaller slips in the kata's text:
- "Note the supertrait:
Ownedmust implementBorrow<Self>."ToOwnedhas no supertrait. Its declaration ispub trait ToOwned { type Owned: Borrow<Self>; … }, so the requirement is a bound on the associated type. A supertrait would be writtentrait ToOwned: Something. The difference matters when you write the impl: nothing is required ofUsernameRefitself, only of the type you pick asOwned. Supertraits has the real thing. - "This is the pattern std uses for
str/String,[T]/Vec<T>,Path/PathBuf,OsStr/OsString." Only half of that holds.PathandOsStrare#[repr(transparent)]structs, butstrand[T]are primitive types, not wrappers. And none of the four transforms anything into_owned. u.borrow() == rdoes not compile unless you name the type. A type with a hand-writtenBorrowimpl has two: yours and the blanketimpl<T: ?Sized> Borrow<T> for T. Sotidy.to_owned().borrow() == tidyisE0283, "type annotations needed", listing "multipleimpls satisfyingUsername: Borrow<_>found". The kata's test avoids it withlet r: &UsernameRef = u.borrow();, which is the fix.
Clippy's pedantic group adds ref_as_ptr on the cast, suggesting std::ptr::from_ref::<str>(s), which every program on this page uses. It also adds redundant_closure_for_method_calls on .map(|w| w.to_lowercase()) and must_use_candidate on the public functions.
Practice¶
Repair the username kata. Keep the canonical form from Kata 3: lowercase, trimmed, internal whitespace collapsed to one -. Keep the unsized #[repr(transparent)] UsernameRef(str). Make to_owned a copy.
Write Username::new(raw: &str) -> Username, which canonicalizes anything. Write UsernameRef::parse(s: &str) -> Result<&UsernameRef, NotCanonical>, which accepts only text that is already canonical and suggests the canonical form when it refuses. Put the pair in a module so that neither the field nor the cast can be reached from outside.
Then run the three checks from Kata 3's part of the run again: the round trip, Cow::to_mut, and the map lookup by the borrowed form. Before you run them, say which of Kata 3's four tests still need changing, and what canonical(s) becomes.
Solution
to_owned_katas_checked_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: repair the username pair so that `to_owned` is a copy.
//! The canonicalizing moves to the only doors in — `Username::new` for raw
//! text, `UsernameRef::parse` for text that must already be canonical — and
//! then `Borrow`, `Cow` and `HashMap` agree with each other again.
//!
//! rustc --edition 2024 to_owned_katas_checked_kata.rs -o /tmp/tokck && /tmp/tokck
use std::borrow::{Borrow, Cow};
use std::collections::HashMap;
mod username {
use std::borrow::Borrow;
use std::fmt;
/// Always canonical: lowercase words joined by '-'.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Username(String);
/// Always canonical too — that is what makes it `Username`'s borrowed half.
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct UsernameRef(str);
#[derive(Debug)]
pub struct NotCanonical {
pub suggestion: Username,
}
fn canonicalize(raw: &str) -> String {
let words: Vec<String> = raw.split_whitespace().map(str::to_lowercase).collect();
words.join("-")
}
impl Username {
/// Door 1: any text in, canonical out. The transform lives here.
pub fn new(raw: &str) -> Username {
Username(canonicalize(raw))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl UsernameRef {
/// Door 2: borrow without allocating, but only text that is already canonical.
pub fn parse(s: &str) -> Result<&UsernameRef, NotCanonical> {
let canonical = canonicalize(s);
if canonical == s {
Ok(UsernameRef::from_str_unchecked(s))
} else {
Err(NotCanonical { suggestion: Username(canonical) })
}
}
/// Private: callers reach it only through a check, or from a `Username`.
fn from_str_unchecked(s: &str) -> &UsernameRef {
// SAFETY: UsernameRef is #[repr(transparent)] over str.
unsafe { &*(std::ptr::from_ref::<str>(s) as *const UsernameRef) }
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Borrow<UsernameRef> for Username {
fn borrow(&self) -> &UsernameRef {
UsernameRef::from_str_unchecked(&self.0)
}
}
impl ToOwned for UsernameRef {
type Owned = Username;
/// A copy, like `str`'s and `Path`'s: the value was checked on the way in.
fn to_owned(&self) -> Username {
Username(self.0.to_owned())
}
}
impl fmt::Display for UsernameRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
}
use username::{Username, UsernameRef};
fn main() {
println!("1. The kata's four tests, with Username::new as the canonical() helper");
let canonical = Username::new;
assert_eq!(canonical(" Alice ").as_str(), "alice");
assert_eq!(canonical("Foo Bar\tBaz").as_str(), "foo-bar-baz");
assert_eq!(canonical("bob").as_str(), "bob");
let u = canonical(" Grace Hopper ");
let r: &UsernameRef = u.borrow();
assert_eq!(r.as_str(), "grace-hopper");
assert_eq!(r.to_owned(), u);
println!(" \" Alice \" -> {:?}", canonical(" Alice ").as_str());
println!(" \"Foo Bar\\tBaz\" -> {:?}", canonical("Foo Bar\tBaz").as_str());
println!(" \"bob\" -> {:?}", canonical("bob").as_str());
println!(" borrow round trip -> {r}, r.to_owned() == u: {}", r.to_owned() == u);
println!(" all four assertions hold, unchanged");
println!();
println!("2. Raw text can no longer become a UsernameRef");
match UsernameRef::parse(" Grace Hopper ") {
Ok(r) => println!(" parsed {r}"),
Err(e) => println!(" parse(\" Grace Hopper \") -> Err, did you mean {:?}?", e.suggestion),
}
let tidy = UsernameRef::parse("grace-hopper").expect("already canonical");
println!(" parse(\"grace-hopper\") -> Ok({tidy})");
println!();
println!("3. The checks the transforming to_owned failed, run again");
let owned = tidy.to_owned();
let back: &UsernameRef = owned.borrow();
println!(" tidy.to_owned().borrow() == tidy {}", back == tidy);
let mut cow: Cow<'_, UsernameRef> = Cow::Borrowed(tidy);
let before = cow.as_str().to_owned();
let _ = cow.to_mut();
println!(" to_mut() kept the value {}", cow.as_str() == before);
println!(" Cow::Borrowed == Cow::Owned {}", Cow::Borrowed(tidy) == Cow::<UsernameRef>::Owned(tidy.to_owned()));
let mut seats: HashMap<Username, u32> = HashMap::new();
seats.insert(Username::new("Ada"), 3);
let key = UsernameRef::parse("ada").expect("canonical");
println!(" seats.get(parse(\"ada\")) {:?}", seats.get(key));
}
Verified output of to_owned_katas_checked_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The kata's four tests, with Username::new as the canonical() helper
" Alice " -> "alice"
"Foo Bar\tBaz" -> "foo-bar-baz"
"bob" -> "bob"
borrow round trip -> grace-hopper, r.to_owned() == u: true
all four assertions hold, unchanged
2. Raw text can no longer become a UsernameRef
parse(" Grace Hopper ") -> Err, did you mean Username("grace-hopper")?
parse("grace-hopper") -> Ok(grace-hopper)
3. The checks the transforming to_owned failed, run again
tidy.to_owned().borrow() == tidy true
to_mut() kept the value true
Cow::Borrowed == Cow::Owned true
seats.get(parse("ada")) Some(3)
None of the four tests changes except the helper: canonical(s) becomes Username::new(s). The three that canonicalize raw input test new, and borrow_roundtrip already started from a Username. What you can no longer write is UsernameRef::new(" Alice "). That was the source of every failure in the run.
Where this leaves you¶
- The trap in Kata 2, one step at a time: step 5, the dot takes the first receiver that fits, then step 6, the blanket impl, whose kata is Kata 2 with the trap made visible.
- Kata 3's pattern done properly, with a checked door,
HashMapandCow: ImplementingToOwnedfor your own type, and its kata, a slice that promises its order. - The promise
Borrowdoes write down, and an impl that breaks it:Borrow. - What
to_mutandinto_owneddo on a healthyCow: step 7 andCow: borrow until somebody writes. - A safety invariant versus a logical one. Kata 3's broken design is the logical kind, a wrong answer rather than UB: What an invariant is.
- The same kind of page for
Cowexplanations: WhatCowexplanations get wrong, run. - Every kata in the library, in order: KATAS.md.
Docs: ToOwned::to_owned ↗ · Cow::to_mut ↗ · Borrow ↗ · The transparent representation ↗
Po polsku¶
Trzy krążące razem katy o ToOwned kompilują się i przechodzą własne testy na rustc 1.98.0, ale nie są równie dobre. Kata 1 (.to_owned() na str i wycinkach) jest poprawna i łatwa, a jej dwie pierwsze funkcje to przykład otwierający stronę o ToOwned. Kata 2 (funkcja generyczna T: ?Sized + ToOwned nad &[&T]) jest dobra, a dereferencja (*x) jest potrzebna z podanego powodu: na &&T kropka najpierw trafia na implementację zbiorczą dla &T. Błędne jest jedno słowo, „po cichu”. W funkcji z katy wynik musi być T::Owned, więc kompilator zgłasza E0277. Po cichu dostajesz Vec<&str> dopiero wtedy, gdy nic nie wymaga typu posiadanego, na przykład przy Vec<_>.
Kata 3 uczy wzorca, którego biblioteka standardowa nie stosuje: to_owned, które zmienia wartość (małe litery, myślniki). Wszystkie implementacje ToOwned w std tylko kopiują, a Cow na tym polega, bo to_mut() i into_owned() „klonują” właśnie przez to_owned. Zmierzone skutki: powrót przez borrow() daje inną wartość, to_mut() przepisuje tekst, zanim cokolwiek zapiszesz, a HashMap nie znajduje klucza szukanego tym samym tekstem, od którego zaczęliśmy. Naprawa polega na przeniesieniu kanonizacji do jedynych drzwi wejściowych: Username::new kanonizuje, UsernameRef::parse przyjmuje tylko tekst już kanoniczny, a to_owned z powrotem kopiuje. Przy okazji: ToOwned nie ma nadcechy (supertrait), a Borrow<Self> to ograniczenie typu powiązanego Owned.
Szukaj po polsku: rust ToOwned custom impl Cow · rust Borrow Hash Eq equivalence · rust repr(transparent) newtype str · rust E0283 multiple impls Borrow
How to learn ToOwned › Beside the steps · back to Step 6 · Step 7 · What Cow explanations get wrong, run