Skip to content

← Trait Objects and Dispatch step 10 of 10

Medium Primitives

Choose parameter types and shrink an enum

Reduce the size of Rule by boxing its large Table payload. Update the corresponding constructor in rule_report, keep the supplied function signatures, and preserve the behavior below. Do not suppress the lint or remove the const _: signature checks.

The starter compiles with rustc, but fails Clippy's large_enum_variant check and the harness's requirement that size_of::<Rule>() <= 32 bytes. The size limit is an exercise check on the grading target, not a portable guarantee about Rust enum layout.

Required behavior

pub fn normalise(entries: Vec<String>, banned: Vec<String>) -> Vec<String>
pub fn describe(rule: &Rule) -> String
pub fn rule_report() -> Vec<String>

normalise trims both ends of each entry with str::trim, then lowercases it with str::to_lowercase. Apply the same transformation to banned words. Drop empty results, exact matches in the transformed banned list, and later duplicates. Preserve the order of the first surviving occurrences.

For example, entries [" Alpha ", "beta", "ALPHA", "gamma"] and banned ["Gamma"] produce ["alpha", "beta"]. An empty entries vector produces an empty result. Duplicate or empty banned words have no additional effect.

Text may contain arbitrary Unicode. Trimming uses Unicode whitespace; lowercasing is Unicode-aware and not locale-specific. It can change string length. It does not perform Unicode normalization or full case folding: "Straße" becomes "straße", which remains different from "strasse". Internal whitespace and other characters remain unless lowercasing changes them. Unicode tables can change with Rust versions.

describe returns "allow" for Allow, "ban:" followed by the unchanged string for Ban, and "table:512:" followed by the first byte in decimal for Table. Keep the table length at 512. rule_report returns exactly ["allow", "ban:spam", "table:512:7"].

The harness checks that report, the normalized entries and the enum size. Passing also requires compilation and Clippy with warnings treated as errors.

Box the large payload

Change Table([u8; 512]) to Table(Box<[u8; 512]>), and wrap its array constructor in Box::new. The existing describe implementation can still call len() and index the boxed array through dereferencing.

An inline enum must have enough storage for any of its variants. Boxing the array moves that payload into a separate allocation, reducing the space each enum occupies. It adds allocation and indirect access for Table values. Whether that tradeoff helps a real program depends on its workload.

The whole enum does not become pointer-sized: Ban still contains a String. Alignment and representation choices also matter, and a source-level move does not guarantee a particular machine-level copy. Use size_of to inspect a build; do not infer an exact enum size from its largest field plus a tag. See the Rust layout reference.

Match each parameter to the operation

The helper signatures illustrate three different needs:

Helper Why this parameter fits
is_banned(&str, &[String]) Reads text and a sequence without needing their owning containers.
push_unique(&mut Vec<String>, &str) May append a string, so it needs mutable access to the Vec.
describe(&Rule) Reads a rule while its caller retains ownership.

The const _: assignments ensure those function types still match. They do not prove that a function is pure or enforce its algorithm.

normalise takes ownership of both vectors because that is this exercise's public API. Its implementation consumes them, but its algorithm could also be exposed through a borrowing API. Returning owned strings does not by itself require owned inputs or guarantee reuse of their allocations.

Prefer &str or &[T] when a function only needs the borrowed view. Clippy's ptr_arg helps identify unnecessary container references, but container methods or other API requirements can justify a narrower type. A mutable slice cannot replace the Vec in push_unique, because a slice cannot grow.

Accepting a value that converts into String

impl Into<String> is useful when a function will retain an owned string. This complete example accepts both borrowed text and an existing String:

fn add(names: &mut Vec<String>, name: impl Into<String>) {
    names.push(name.into());
}

fn main() {
    let mut names = Vec::new();
    add(&mut names, "Ada");
    let owned = String::from("Lin");
    add(&mut names, owned);
    assert_eq!(names, ["Ada", "Lin"]);
}

Converting &str creates owned text; converting a String moves that value. The conversion runs inside add. A generic parameter can be instantiated for different argument types, but final code size depends on optimization. For a function that only reads text, &str or an appropriate AsRef<str> bound expresses a different contract: access to a borrowed view rather than conversion into an owned String.