We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Trait Objects and Dispatch step 10 of 10
Designing signatures: T, &T, &mut T, impl Into<T>
You now have the whole vocabulary — ownership, borrowing, lifetimes, traits, trait objects. This item is about using it as a design language. Every parameter you write encodes a promise about who owns what, and getting that wrong is the single most common source of unnecessary allocation and unnecessary friction in Rust code.
The functional task here is deliberately trivial. All the grading pressure comes from clippy.
The decision procedure
| you want to… | take |
|---|---|
| store it, consume it, or transform it into something you return |
T (by value) |
| only read it |
&T — and for String/Vec<T>, &str/&[T] |
| modify it in place, caller keeps it |
&mut T |
| let the caller pass either an owned or a borrowed thing |
impl Into<T> / impl AsRef<str> |
Two of those rows have a lint attached, and both are on by default.
ptr_arg fires on &String, &Vec<T>, &PathBuf. A &String can only
be created from a String; a &str can be created from a String, a string
literal, a slice of another string, or a Cow. Taking the narrower type
excludes callers for no gain whatsoever. The same for &Vec<T> versus
&[T]. (Honest caveat: ptr_arg does not fire in every position — it backs
off inside trait impls, and when the body actually uses Vec-specific API.
Do not assume its silence means your signature is right.)
needless_pass_by_value is the mirror image: you took T by value and
then only read it. It is genuinely the most valuable lint in this area and it
is pedantic, so it is off by default and this problem cannot grade it. Turn
it on in your own projects.
The bad version, so you can recognise it
fn is_banned(word: &String, banned: &Vec<String>) -> bool { // ptr_arg ×2
banned.contains(word)
}
fn tidy(word: String) -> String { // needless_pass_by_value
word.trim().to_lowercase() // ...it only reads
}
fn label(rule: &Box<Rule>) -> String { ... } // borrowed_box
Every one of those compiles and works. Every one of them is wrong in review.
The enum
pub enum Rule {
Allow,
Ban(String),
Table([u8; 512]),
}
An enum is as large as its largest variant plus a discriminant. So this
Rule is over 512 bytes — and a Vec<Rule> of a thousand mostly-Allow
rules burns half a megabyte on padding. Every move of a Rule copies 512
bytes. Every Result<T, Rule> inherits the bloat.
clippy’s large_enum_variant catches it and the fix is to Box the one
offending variant, trading a pointer chase on the rare large case for a small
enum everywhere else. describe keeps compiling unchanged, because Box<T>
derefs to T — bytes.len() and bytes[0] work either way.
(The lint has a configurable threshold via clippy.toml, which does not exist
in this single-file harness. 512 bytes is unambiguously over the default, so
it will definitely fire.)
What to write
pub fn normalise(entries: Vec<String>, banned: Vec<String>) -> Vec<String>
Trim each entry, lowercase it, drop it if it is empty after trimming, drop it
if it matches a banned word (banned words are themselves trimmed and
lowercased first), and drop later duplicates — preserving first-appearance
order. [" Alpha ", "beta", "ALPHA", "gamma"] with banned = ["Gamma"]
gives ["alpha", "beta"].
It is implemented over a small internal API whose shapes are pinned by the
const _: lines at the top of the file:
-
fn is_banned(&str, &[String]) -> bool— a pure reader, borrows only. -
fn push_unique(&mut Vec<String>, &str)— mutates in place. -
pub fn describe(&Rule) -> String— reads an enum by reference.
and normalise itself takes both vectors by value, because it consumes
them: the entries are turned into the output, and the banned list is rebuilt
normalised. Leave the pins in place.
Everything except the enum already compiles. Your job is the layout fix — and then to read the rest with the table above in mind, because the same file shows you what each row looks like in practice.
The impl Into<T> argument, which is not settled
fn add(&mut self, name: impl Into<String>) { self.names.push(name.into()) }
Callers may pass "literal" or an owned String, and the allocation decision
moves to the call site — genuinely nicer to use. The counter-argument is real
too: it monomorphises (one copy per argument type), it makes rustdoc and error
messages noisier, and it hides where the allocation happens. The community
does not agree about this in public APIs. Know both sides; do not treat either
as the rule.
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.